chore: resolve merge conflicts

This commit is contained in:
Scott Wilson
2025-01-21 21:30:18 -08:00
248 changed files with 6864 additions and 4051 deletions
+54 -4
View File
@@ -5,6 +5,10 @@ permissions:
id-token: write id-token: write
contents: read contents: read
concurrency:
group: "infisical-core-deployment"
cancel-in-progress: false
jobs: jobs:
infisical-tests: infisical-tests:
name: Integration tests name: Integration tests
@@ -113,10 +117,6 @@ jobs:
steps: steps:
- uses: twingate/github-action@v1 - uses: twingate/github-action@v1
with: with:
# The Twingate Service Key used to connect Twingate to the proper service
# Learn more about [Twingate Services](https://docs.twingate.com/docs/services)
#
# Required
service-key: ${{ secrets.TWINGATE_SERVICE_KEY }} service-key: ${{ secrets.TWINGATE_SERVICE_KEY }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -159,6 +159,31 @@ jobs:
service: infisical-core-platform service: infisical-core-platform
cluster: infisical-core-platform cluster: infisical-core-platform
wait-for-service-stability: true wait-for-service-stability: true
- name: Post slack message
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_DEPLOYMENT_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "*Deployment Status Update*: ${{ job.status }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Deployment Status Update*: ${{ job.status }}"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Application:*\nInfisical Core"
- type: "mrkdwn"
text: "*Instance Type:*\nShared Infisical Cloud"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Region:*\nUS"
- type: "mrkdwn"
text: "*Git Tag:*\n<https://github.com/Infisical/infisical/commit/${{ steps.commit.outputs.short }}>"
production-eu: production-eu:
name: EU production deploy name: EU production deploy
@@ -210,3 +235,28 @@ jobs:
service: infisical-core-platform service: infisical-core-platform
cluster: infisical-core-platform cluster: infisical-core-platform
wait-for-service-stability: true wait-for-service-stability: true
- name: Post slack message
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_DEPLOYMENT_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "*Deployment Status Update*: ${{ job.status }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Deployment Status Update*: ${{ job.status }}"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Application:*\nInfisical Core"
- type: "mrkdwn"
text: "*Instance Type:*\nShared Infisical Cloud"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Region:*\nEU"
- type: "mrkdwn"
text: "*Git Tag:*\n<https://github.com/Infisical/infisical/commit/${{ steps.commit.outputs.short }}>"
@@ -0,0 +1,49 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
// find any duplicate group names within organizations
const duplicates = await knex(TableName.Groups)
.select("orgId", "name")
.count("* as count")
.groupBy("orgId", "name")
.having(knex.raw("count(*) > 1"));
// for each set of duplicates, update all but one with a numbered suffix
for await (const duplicate of duplicates) {
const groups = await knex(TableName.Groups)
.select("id", "name")
.where({
orgId: duplicate.orgId,
name: duplicate.name
})
.orderBy("createdAt", "asc"); // keep original name for oldest group
// skip the first (oldest) group, rename others with numbered suffix
for (let i = 1; i < groups.length; i += 1) {
// eslint-disable-next-line no-await-in-loop
await knex(TableName.Groups)
.where("id", groups[i].id)
.update({
name: `${groups[i].name} (${i})`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TS doesn't know about Knex's timestamp types
updatedAt: new Date()
});
}
}
// add the unique constraint
await knex.schema.alterTable(TableName.Groups, (t) => {
t.unique(["orgId", "name"]);
});
}
export async function down(knex: Knex): Promise<void> {
// Remove the unique constraint
await knex.schema.alterTable(TableName.Groups, (t) => {
t.dropUnique(["orgId", "name"]);
});
}
@@ -0,0 +1,33 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasEnforceCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "enforceCapitalization");
const hasAutoCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "autoCapitalization");
await knex.schema.alterTable(TableName.Project, (t) => {
if (!hasEnforceCapitalizationCol) {
t.boolean("enforceCapitalization").defaultTo(false).notNullable();
}
if (hasAutoCapitalizationCol) {
t.boolean("autoCapitalization").defaultTo(false).alter();
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasEnforceCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "enforceCapitalization");
const hasAutoCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "autoCapitalization");
await knex.schema.alterTable(TableName.Project, (t) => {
if (hasEnforceCapitalizationCol) {
t.dropColumn("enforceCapitalization");
}
if (hasAutoCapitalizationCol) {
t.boolean("autoCapitalization").defaultTo(true).alter();
}
});
}
+3 -2
View File
@@ -13,7 +13,7 @@ export const ProjectsSchema = z.object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
slug: z.string(), slug: z.string(),
autoCapitalization: z.boolean().default(true).nullable().optional(), autoCapitalization: z.boolean().default(false).nullable().optional(),
orgId: z.string().uuid(), orgId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
@@ -25,7 +25,8 @@ export const ProjectsSchema = z.object({
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(), kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(), kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
description: z.string().nullable().optional(), description: z.string().nullable().optional(),
type: z.string() type: z.string(),
enforceCapitalization: z.boolean().default(false)
}); });
export type TProjects = z.infer<typeof ProjectsSchema>; export type TProjects = z.infer<typeof ProjectsSchema>;
@@ -93,7 +93,7 @@ export const auditLogStreamServiceFactory = ({
} }
) )
.catch((err) => { .catch((err) => {
throw new Error(`Failed to connect with the source ${(err as Error)?.message}`); throw new BadRequestError({ message: `Failed to connect with upstream source: ${(err as Error)?.message}` });
}); });
const encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined; const encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined;
const logStream = await auditLogStreamDAL.create({ const logStream = await auditLogStreamDAL.create({
+50 -21
View File
@@ -32,7 +32,7 @@ type TGroupServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">; userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
groupDAL: Pick< groupDAL: Pick<
TGroupDALFactory, TGroupDALFactory,
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" "create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction"
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">; orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
@@ -88,12 +88,26 @@ export const groupServiceFactory = ({
if (!hasRequiredPriviledges) if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" }); throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
const group = await groupDAL.create({ const group = await groupDAL.transaction(async (tx) => {
name, const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`), if (existingGroup) {
orgId: actorOrgId, throw new BadRequestError({
role: isCustomRole ? OrgMembershipRole.Custom : role, message: `Failed to create group with name '${name}'. Group with the same name already exists`
roleId: customRole?.id });
}
const newGroup = await groupDAL.create(
{
name,
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`),
orgId: actorOrgId,
role: isCustomRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id
},
tx
);
return newGroup;
}); });
return group; return group;
@@ -145,21 +159,36 @@ export const groupServiceFactory = ({
if (isCustomRole) customRole = customOrgRole; if (isCustomRole) customRole = customOrgRole;
} }
const [updatedGroup] = await groupDAL.update( const updatedGroup = await groupDAL.transaction(async (tx) => {
{ if (name) {
id: group.id const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
},
{ if (existingGroup && existingGroup.id !== id) {
name, throw new BadRequestError({
slug: slug ? slugify(slug) : undefined, message: `Failed to update group with name '${name}'. Group with the same name already exists`
...(role });
? { }
role: customRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id ?? null
}
: {})
} }
);
const [updated] = await groupDAL.update(
{
id: group.id
},
{
name,
slug: slug ? slugify(slug) : undefined,
...(role
? {
role: customRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id ?? null
}
: {})
},
tx
);
return updated;
});
return updatedGroup; return updatedGroup;
}; };
@@ -476,14 +476,14 @@ export const ldapConfigServiceFactory = ({
}); });
} else { } else {
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed // limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members." message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members."
}); });
} }
if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members." message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members."
@@ -421,14 +421,14 @@ export const samlConfigServiceFactory = ({
}); });
} else { } else {
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed // limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members." message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members."
}); });
} }
if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members." message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members."
+16 -1
View File
@@ -531,7 +531,7 @@ export const scimServiceFactory = ({
firstName: scimUser.name.givenName, firstName: scimUser.name.givenName,
email: scimUser.emails[0].value, email: scimUser.emails[0].value,
lastName: scimUser.name.familyName, lastName: scimUser.name.familyName,
isEmailVerified: hasEmailChanged ? trustScimEmails : true isEmailVerified: hasEmailChanged ? trustScimEmails : undefined
}, },
tx tx
); );
@@ -790,6 +790,21 @@ export const scimServiceFactory = ({
}); });
const newGroup = await groupDAL.transaction(async (tx) => { const newGroup = await groupDAL.transaction(async (tx) => {
const conflictingGroup = await groupDAL.findOne(
{
name: displayName,
orgId
},
tx
);
if (conflictingGroup) {
throw new ScimRequestError({
detail: `Group with name '${displayName}' already exists in the organization`,
status: 409
});
}
const group = await groupDAL.create( const group = await groupDAL.create(
{ {
name: displayName, name: displayName,
@@ -1267,9 +1267,10 @@ export const secretApprovalRequestServiceFactory = ({
type: SecretType.Shared type: SecretType.Shared
})) }))
); );
if (secrets.length)
if (secrets.length !== secretsWithNewName.length)
throw new NotFoundError({ throw new NotFoundError({
message: `Secret does not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}` message: `Secret does not exist: ${secrets.map((el) => el.key).join(",")}`
}); });
} }
+1 -1
View File
@@ -476,7 +476,7 @@ export const PROJECTS = {
}, },
ADD_GROUP_TO_PROJECT: { ADD_GROUP_TO_PROJECT: {
projectId: "The ID of the project to add the group to.", projectId: "The ID of the project to add the group to.",
groupId: "The ID of the group to add to the project.", groupIdOrName: "The ID or name of the group to add to the project.",
role: "The role for the group to assume in the project." role: "The role for the group to assume in the project."
}, },
UPDATE_GROUP_IN_PROJECT: { UPDATE_GROUP_IN_PROJECT: {
+1
View File
@@ -1,3 +1,4 @@
export { isDisposableEmail } from "./validate-email"; export { isDisposableEmail } from "./validate-email";
export { isValidFolderName, isValidSecretPath } from "./validate-folder-name"; export { isValidFolderName, isValidSecretPath } from "./validate-folder-name";
export { blockLocalAndPrivateIpAddresses } from "./validate-url"; export { blockLocalAndPrivateIpAddresses } from "./validate-url";
export { isUuidV4 } from "./validate-uuid";
@@ -0,0 +1,3 @@
import { z } from "zod";
export const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success;
+2 -1
View File
@@ -923,7 +923,8 @@ export const registerRoutes = async (
certificateTemplateDAL, certificateTemplateDAL,
projectSlackConfigDAL, projectSlackConfigDAL,
slackIntegrationDAL, slackIntegrationDAL,
projectTemplateService projectTemplateService,
groupProjectDAL
}); });
const projectEnvService = projectEnvServiceFactory({ const projectEnvService = projectEnvServiceFactory({
@@ -73,6 +73,40 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
url: "/signup-resend",
config: {
rateLimit: inviteUserRateLimit
},
method: "POST",
schema: {
body: z.object({
membershipId: z.string()
}),
response: {
200: z.object({
signupToken: z
.object({
email: z.string(),
link: z.string()
})
.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
return server.services.org.resendOrgMemberInvitation({
orgId: req.permission.orgId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
membershipId: req.body.membershipId
});
}
});
server.route({ server.route({
url: "/verify", url: "/verify",
method: "POST", method: "POST",
@@ -16,7 +16,7 @@ import { ProjectUserMembershipTemporaryMode } from "@app/services/project-member
export const registerGroupProjectRouter = async (server: FastifyZodProvider) => { export const registerGroupProjectRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
url: "/:projectId/groups/:groupId", url: "/:projectId/groups/:groupIdOrName",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
@@ -30,7 +30,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
], ],
params: z.object({ params: z.object({
projectId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectId), projectId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectId),
groupId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupId) groupIdOrName: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupIdOrName)
}), }),
body: z body: z
.object({ .object({
@@ -76,7 +76,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
roles: req.body.roles || [{ role: req.body.role }], roles: req.body.roles || [{ role: req.body.role }],
projectId: req.params.projectId, projectId: req.params.projectId,
groupId: req.params.groupId groupIdOrName: req.params.groupIdOrName
}); });
return { groupMembership }; return { groupMembership };
@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import ms from "ms"; import ms from "ms";
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding } from "@app/db/schemas"; import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
@@ -9,6 +9,7 @@ import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { isUuidV4 } from "@app/lib/validator";
import { TGroupDALFactory } from "../../ee/services/group/group-dal"; import { TGroupDALFactory } from "../../ee/services/group/group-dal";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
@@ -62,7 +63,7 @@ export const groupProjectServiceFactory = ({
actorAuthMethod, actorAuthMethod,
roles, roles,
projectId, projectId,
groupId groupIdOrName
}: TCreateProjectGroupDTO) => { }: TCreateProjectGroupDTO) => {
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
@@ -79,13 +80,20 @@ export const groupProjectServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); let group: TGroups | null = null;
if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); if (isUuidV4(groupIdOrName)) {
group = await groupDAL.findOne({ orgId: actorOrgId, id: groupIdOrName });
}
if (!group) {
group = await groupDAL.findOne({ orgId: actorOrgId, name: groupIdOrName });
}
if (!group) throw new NotFoundError({ message: `Failed to find group with ID or name ${groupIdOrName}` });
const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
if (existingGroup) if (existingGroup)
throw new BadRequestError({ throw new BadRequestError({
message: `Group with ID ${groupId} already exists in project with id ${project.id}` message: `Group with ID ${group.id} already exists in project with id ${project.id}`
}); });
for await (const { role: requestedRoleChange } of roles) { for await (const { role: requestedRoleChange } of roles) {
@@ -128,7 +136,7 @@ export const groupProjectServiceFactory = ({
const projectGroup = await groupProjectDAL.transaction(async (tx) => { const projectGroup = await groupProjectDAL.transaction(async (tx) => {
const groupProjectMembership = await groupProjectDAL.create( const groupProjectMembership = await groupProjectDAL.create(
{ {
groupId: group.id, groupId: group!.id,
projectId: project.id projectId: project.id
}, },
tx tx
@@ -163,7 +171,7 @@ export const groupProjectServiceFactory = ({
// share project key with users in group that have not // share project key with users in group that have not
// individually been added to the project and that are not part of // individually been added to the project and that are not part of
// other groups that are in the project // other groups that are in the project
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id, tx); const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx);
if (groupMembers.length) { if (groupMembers.length) {
const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx); const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx);
@@ -3,7 +3,7 @@ import { TProjectPermission } from "@app/lib/types";
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
export type TCreateProjectGroupDTO = { export type TCreateProjectGroupDTO = {
groupId: string; groupIdOrName: string;
roles: ( roles: (
| { | {
role: string; role: string;
@@ -932,15 +932,22 @@ const syncSecretsAWSParameterStore = async ({
logger.info( logger.info(
`getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}]` `getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}]`
); );
await ssm
.putParameter({ try {
Name: `${integration.path}${key}`, await ssm
Type: "SecureString", .putParameter({
Value: secrets[key].value, Name: `${integration.path}${key}`,
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }), Type: "SecureString",
Overwrite: true Value: secrets[key].value,
}) ...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }),
.promise(); Overwrite: true
})
.promise();
} catch (error) {
(error as { secretKey: string }).secretKey = key;
throw error;
}
if (metadata.secretAWSTag?.length) { if (metadata.secretAWSTag?.length) {
try { try {
await ssm await ssm
@@ -987,15 +994,20 @@ const syncSecretsAWSParameterStore = async ({
// we ensure that the KMS key configured in the integration is applied for ALL parameters on AWS // we ensure that the KMS key configured in the integration is applied for ALL parameters on AWS
if (secrets[key].value && (shouldUpdateKms || awsParameterStoreSecretsObj[key].Value !== secrets[key].value)) { if (secrets[key].value && (shouldUpdateKms || awsParameterStoreSecretsObj[key].Value !== secrets[key].value)) {
await ssm try {
.putParameter({ await ssm
Name: `${integration.path}${key}`, .putParameter({
Type: "SecureString", Name: `${integration.path}${key}`,
Value: secrets[key].value, Type: "SecureString",
Overwrite: true, Value: secrets[key].value,
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }) Overwrite: true,
}) ...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId })
.promise(); })
.promise();
} catch (error) {
(error as { secretKey: string }).secretKey = key;
throw error;
}
} }
if (awsParameterStoreSecretsObj[key].Name) { if (awsParameterStoreSecretsObj[key].Name) {
+64 -1
View File
@@ -69,6 +69,7 @@ import {
TGetOrgMembershipDTO, TGetOrgMembershipDTO,
TInviteUserToOrgDTO, TInviteUserToOrgDTO,
TListProjectMembershipsByOrgMembershipIdDTO, TListProjectMembershipsByOrgMembershipIdDTO,
TResendOrgMemberInvitationDTO,
TUpdateOrgDTO, TUpdateOrgDTO,
TUpdateOrgMembershipDTO, TUpdateOrgMembershipDTO,
TVerifyUserToOrgDTO TVerifyUserToOrgDTO
@@ -584,6 +585,66 @@ export const orgServiceFactory = ({
}); });
return membership; return membership;
}; };
const resendOrgMemberInvitation = async ({
orgId,
actorId,
actor,
actorAuthMethod,
actorOrgId,
membershipId
}: TResendOrgMemberInvitationDTO) => {
const appCfg = getConfig();
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
const org = await orgDAL.findOrgById(orgId);
const [inviteeOrgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
[`${TableName.OrgMembership}.id` as "id"]: membershipId
});
if (inviteeOrgMembership.status !== OrgMembershipStatus.Invited) {
throw new BadRequestError({
message: "Organization invitation already accepted"
});
}
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: inviteeOrgMembership.userId,
orgId
});
if (!appCfg.isSmtpConfigured) {
return {
signupToken: {
email: inviteeOrgMembership.email as string,
link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeOrgMembership.email}&organization_id=${org?.id}`
}
};
}
await smtpService.sendMail({
template: SmtpTemplates.OrgInvite,
subjectLine: "Infisical organization invitation",
recipients: [inviteeOrgMembership.email as string],
substitutions: {
inviterFirstName: inviteeOrgMembership.firstName,
inviterUsername: inviteeOrgMembership.email,
organizationName: org?.name,
email: inviteeOrgMembership.email,
organizationId: org?.id.toString(),
token,
callback_url: `${appCfg.SITE_URL}/signupinvite`
}
});
return { signupToken: undefined };
};
/* /*
* Invite user to organization * Invite user to organization
*/ */
@@ -627,6 +688,7 @@ export const orgServiceFactory = ({
} }
}) })
: []; : [];
if (projectsToInvite.length !== invitedProjects?.length) { if (projectsToInvite.length !== invitedProjects?.length) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Access denied to one or more of the specified projects" message: "Access denied to one or more of the specified projects"
@@ -1221,6 +1283,7 @@ export const orgServiceFactory = ({
deleteIncidentContact, deleteIncidentContact,
getOrgGroups, getOrgGroups,
listProjectMembershipsByOrgMembershipId, listProjectMembershipsByOrgMembershipId,
findOrgBySlug findOrgBySlug,
resendOrgMemberInvitation
}; };
}; };
+4
View File
@@ -35,6 +35,10 @@ export type TInviteUserToOrgDTO = {
}[]; }[];
} & TOrgPermission; } & TOrgPermission;
export type TResendOrgMemberInvitationDTO = {
membershipId: string;
} & TOrgPermission;
export type TVerifyUserToOrgDTO = { export type TVerifyUserToOrgDTO = {
email: string; email: string;
orgId: string; orgId: string;
@@ -29,6 +29,7 @@ import { ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal"; import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
@@ -100,7 +101,8 @@ type TProjectServiceFactoryDep = {
identityProjectDAL: TIdentityProjectDALFactory; identityProjectDAL: TIdentityProjectDALFactory;
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">; identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne" | "delete">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "delete">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">; projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">; slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
@@ -120,7 +122,7 @@ type TProjectServiceFactoryDep = {
orgDAL: Pick<TOrgDALFactory, "findOne">; orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">; keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">; projectBotDAL: Pick<TProjectBotDALFactory, "create">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany" | "delete">;
kmsService: Pick< kmsService: Pick<
TKmsServiceFactory, TKmsServiceFactory,
| "updateProjectSecretManagerKmsKey" | "updateProjectSecretManagerKmsKey"
@@ -169,7 +171,8 @@ export const projectServiceFactory = ({
projectBotDAL, projectBotDAL,
projectSlackConfigDAL, projectSlackConfigDAL,
slackIntegrationDAL, slackIntegrationDAL,
projectTemplateService projectTemplateService,
groupProjectDAL
}: TProjectServiceFactoryDep) => { }: TProjectServiceFactoryDep) => {
/* /*
* Create workspace. Make user the admin * Create workspace. Make user the admin
@@ -444,13 +447,32 @@ export const projectServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
const deletedProject = await projectDAL.transaction(async (tx) => { const deletedProject = await projectDAL.transaction(async (tx) => {
// delete these so that project custom roles can be deleted in cascade effect
// direct deletion of project without these will cause fk error
await projectMembershipDAL.delete({ projectId: project.id }, tx);
await groupProjectDAL.delete({ projectId: project.id }, tx);
const delProject = await projectDAL.deleteById(project.id, tx); const delProject = await projectDAL.deleteById(project.id, tx);
const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id, tx).catch(() => null); const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id, tx).catch(() => null);
// akhilmhdh: before removing those kms checking any other project uses it
// happened due to project split
if (delProject.kmsCertificateKeyId) { if (delProject.kmsCertificateKeyId) {
await kmsService.deleteInternalKms(delProject.kmsCertificateKeyId, delProject.orgId, tx); const projectsLinkedToForiegnKey = await projectDAL.find(
{ kmsCertificateKeyId: delProject.kmsCertificateKeyId },
{ tx }
);
if (!projectsLinkedToForiegnKey.length) {
await kmsService.deleteInternalKms(delProject.kmsCertificateKeyId, delProject.orgId, tx);
}
} }
if (delProject.kmsSecretManagerKeyId) { if (delProject.kmsSecretManagerKeyId) {
await kmsService.deleteInternalKms(delProject.kmsSecretManagerKeyId, delProject.orgId, tx); const projectsLinkedToForiegnKey = await projectDAL.find(
{ kmsSecretManagerKeyId: delProject.kmsSecretManagerKeyId },
{ tx }
);
if (!projectsLinkedToForiegnKey.length) {
await kmsService.deleteInternalKms(delProject.kmsSecretManagerKeyId, delProject.orgId, tx);
}
} }
// Delete the org membership for the ghost user if it's found. // Delete the org membership for the ghost user if it's found.
if (projectGhostUser) { if (projectGhostUser) {
@@ -544,8 +566,10 @@ export const projectServiceFactory = ({
const updatedProject = await projectDAL.updateById(project.id, { const updatedProject = await projectDAL.updateById(project.id, {
name: update.name, name: update.name,
description: update.description, description: update.description,
autoCapitalization: update.autoCapitalization autoCapitalization: update.autoCapitalization,
enforceCapitalization: update.autoCapitalization
}); });
return updatedProject; return updatedProject;
}; };
@@ -567,7 +591,11 @@ export const projectServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
const updatedProject = await projectDAL.updateById(projectId, { autoCapitalization }); const updatedProject = await projectDAL.updateById(projectId, {
autoCapitalization,
enforceCapitalization: autoCapitalization
});
return updatedProject; return updatedProject;
}; };
@@ -1,12 +1,12 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { z } from "zod";
import { TSecretSharing } from "@app/db/schemas"; import { TSecretSharing } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { SecretSharingAccessType } from "@app/lib/types"; import { SecretSharingAccessType } from "@app/lib/types";
import { isUuidV4 } from "@app/lib/validator";
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
@@ -28,8 +28,6 @@ type TSecretSharingServiceFactoryDep = {
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>; export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success;
export const secretSharingServiceFactory = ({ export const secretSharingServiceFactory = ({
permissionService, permissionService,
secretSharingDAL, secretSharingDAL,
+52 -1
View File
@@ -88,7 +88,7 @@ type TSecretServiceFactoryDep = {
secretDAL: TSecretDALFactory; secretDAL: TSecretDALFactory;
secretTagDAL: TSecretTagDALFactory; secretTagDAL: TSecretTagDALFactory;
secretVersionDAL: TSecretVersionDALFactory; secretVersionDAL: TSecretVersionDALFactory;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug" | "findById">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
folderDAL: Pick< folderDAL: Pick<
TSecretFolderDALFactory, TSecretFolderDALFactory,
@@ -1466,6 +1466,16 @@ export const secretServiceFactory = ({
secretMetadata secretMetadata
}: TCreateSecretRawDTO) => { }: TCreateSecretRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
const project = await projectDAL.findById(projectId);
if (project.enforceCapitalization) {
if (secretName !== secretName.toUpperCase()) {
throw new BadRequestError({
message:
"Secret name must be in UPPERCASE per project requirements. You can disable this requirement in project settings."
});
}
}
const policy = const policy =
actor === ActorType.USER && type === SecretType.Shared actor === ActorType.USER && type === SecretType.Shared
? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath) ? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath)
@@ -1609,6 +1619,16 @@ export const secretServiceFactory = ({
secretMetadata secretMetadata
}: TUpdateSecretRawDTO) => { }: TUpdateSecretRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
const project = await projectDAL.findById(projectId);
if (project.enforceCapitalization) {
if (newSecretName && newSecretName !== newSecretName.toUpperCase()) {
throw new BadRequestError({
message:
"Secret name must be in UPPERCASE per project requirements. You can disable this requirement in project settings."
});
}
}
const policy = const policy =
actor === ActorType.USER && type === SecretType.Shared actor === ActorType.USER && type === SecretType.Shared
? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath) ? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath)
@@ -1858,7 +1878,23 @@ export const secretServiceFactory = ({
actor === ActorType.USER actor === ActorType.USER
? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath) ? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath)
: undefined; : undefined;
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
const project = await projectDAL.findById(projectId);
if (project.enforceCapitalization) {
const caseViolatingSecretKeys = inputSecrets
.filter((sec) => sec.secretKey !== sec.secretKey.toUpperCase())
.map((sec) => sec.secretKey);
if (caseViolatingSecretKeys.length) {
throw new BadRequestError({
message: `Secret names must be in UPPERCASE per project requirements: ${caseViolatingSecretKeys.join(
", "
)}. You can disable this requirement in project settings`
});
}
}
if (policy) { if (policy) {
const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({
policy, policy,
@@ -1987,6 +2023,21 @@ export const secretServiceFactory = ({
? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath) ? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath)
: undefined; : undefined;
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
const project = await projectDAL.findById(projectId);
if (project.enforceCapitalization) {
const caseViolatingSecretKeys = inputSecrets
.filter((sec) => sec.newSecretName && sec.newSecretName !== sec.newSecretName.toUpperCase())
.map((sec) => sec.newSecretName);
if (caseViolatingSecretKeys.length) {
throw new BadRequestError({
message: `Secret names must be in UPPERCASE per project requirements: ${caseViolatingSecretKeys.join(
", "
)}. You can disable this requirement in project settings`
});
}
}
if (policy) { if (policy) {
const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({
policy, policy,
@@ -5,6 +5,7 @@ description: "Learn how to use the InfisicalSecret CRD to fetch secrets from Inf
--- ---
Once you have installed the operator to your cluster, you'll need to create a `InfisicalSecret` custom resource definition (CRD). Once you have installed the operator to your cluster, you'll need to create a `InfisicalSecret` custom resource definition (CRD).
In this CRD, you'll define the authentication method to use, the secrets to fetch, and the target location to store the secrets within your cluster.
```yaml example-infisical-secret-crd.yaml ```yaml example-infisical-secret-crd.yaml
apiVersion: secrets.infisical.com/v1alpha1 apiVersion: secrets.infisical.com/v1alpha1
@@ -19,101 +20,28 @@ spec:
hostAPI: https://app.infisical.com/api hostAPI: https://app.infisical.com/api
resyncInterval: 10 resyncInterval: 10
authentication: authentication:
# Make sure to only have 1 authentication method defined, serviceToken/universalAuth.
# If you have multiple authentication methods defined, it may cause issues.
# (Deprecated) Service Token Auth
serviceToken:
serviceTokenSecretReference:
secretName: service-token
secretNamespace: default
secretsScope:
envSlug: <env-slug>
secretsPath: <secrets-path>
recursive: true
# Universal Auth
universalAuth:
secretsScope:
projectSlug: new-ob-em
envSlug: dev # "dev", "staging", "prod", etc..
secretsPath: "/" # Root is "/"
recursive: true # Whether or not to use recursive mode (Fetches all secrets in an environment from a given secret path, and all folders inside the path) / defaults to false
credentialsRef:
secretName: universal-auth-credentials
secretNamespace: default
# Native Kubernetes Auth
kubernetesAuth: kubernetesAuth:
identityId: <machine-identity-id> identityId: <machine-identity-id>
serviceAccountRef: serviceAccountRef:
name: <service-account-name> name: <service-account-name>
namespace: <service-account-namespace> namespace: <service-account-namespace>
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
secretsScope:
projectSlug: your-project-slug
envSlug: prod
secretsPath: "/path"
recursive: true
# AWS IAM Auth
awsIamAuth:
identityId: <your-machine-identity-id>
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
secretsScope:
projectSlug: your-project-slug
envSlug: prod
secretsPath: "/path"
recursive: true
# Azure Auth
azureAuth:
identityId: <your-machine-identity-id>
resource: https://management.azure.com/&client_id=CLIENT_ID # (Optional) This is the Azure resource that you want to access. For example, "https://management.azure.com/". If no value is provided, it will default to "https://management.azure.com/"
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
secretsScope:
projectSlug: your-project-slug
envSlug: prod
secretsPath: "/path"
recursive: true
# GCP ID Token Auth
gcpIdTokenAuth:
identityId: <your-machine-identity-id>
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
secretsScope:
projectSlug: your-project-slug
envSlug: prod
secretsPath: "/path"
recursive: true
# GCP IAM Auth
gcpIamAuth:
identityId: <your-machine-identity-id>
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
secretsScope:
projectSlug: your-project-slug
envSlug: prod
secretsPath: "/path"
recursive: true
managedSecretReference: managedSecretReference:
secretName: managed-secret secretName: managed-secret
secretNamespace: default secretNamespace: default
creationPolicy: "Orphan" ## Owner | Orphan creationPolicy: "Orphan"
# template: template:
# includeAllSecrets: true includeAllSecrets: true
# data: data:
# CUSTOM_KEY: "{{ .KEY.SecretPath }} {{ .KEY.Value }}" NEW_KEY_NAME: "{{ .KEY.SecretPath }} {{ .KEY.Value }}"
# secretType: kubernetes.io/dockerconfigjson KEY_WITH_BINARY_VALUE: "{{ .KEY.SecretPath }} {{ .KEY.Value }}"
``` ```
### InfisicalSecret CRD properties ## CRD properties
### Generic
The following properties help define what instance of Infisical the operator will interact with, the interval it will sync secrets and any CA certificates that may be required to connect.
<Accordion title="hostAPI"> <Accordion title="hostAPI">
If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to
@@ -165,10 +93,12 @@ When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
CA certificate to use for connecting to the Infisical instance with SSL/TLS. CA certificate to use for connecting to the Infisical instance with SSL/TLS.
</Accordion> </Accordion>
<Accordion title="authentication"> ### Authentication methods
This block defines the method that will be used to authenticate with Infisical
so that secrets can be fetched To retrieve the requested secrets, the operator must first authenticate with Infisical.
</Accordion> The list of available authentication methods are shown below.
<Accordion title="authentication"></Accordion>
<Accordion title="authentication.universalAuth"> <Accordion title="authentication.universalAuth">
The universal machine identity authentication method is used to authenticate with Infisical. The client ID and client secret needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores these credentials. The universal machine identity authentication method is used to authenticate with Infisical. The client ID and client secret needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores these credentials.
@@ -605,13 +535,13 @@ spec:
</Accordion> </Accordion>
### Operator managed secrets
The managed secret properties specify where to store the secrets retrieved from your Infisical project.
This includes defining the name and namespace of the Kubernetes secret that will hold these secrets.
The Infisical operator will automatically create the Kubernetes secret in the specified name/namespace and ensure it stays up-to-date.
<Accordion title="managedSecretReference"> <Accordion title="managedSecretReference">
The `managedSecretReference` field is used to define the target location for storing secrets retrieved from an Infisical project.
This field requires specifying both the name and namespace of the Kubernetes secret that will hold these secrets.
The Infisical operator will automatically create the Kubernetes secret with the specified name/namespace and keep it continuously updated.
Note: The managed secret be should be created in the same namespace as the deployment that will use it.
</Accordion> </Accordion>
<Accordion title="managedSecretReference.secretName"> <Accordion title="managedSecretReference.secretName">
The name of the managed Kubernetes secret to be created The name of the managed Kubernetes secret to be created
@@ -621,57 +551,6 @@ The namespace of the managed Kubernetes secret to be created.
</Accordion> </Accordion>
<Accordion title="managedSecretReference.secretType"> <Accordion title="managedSecretReference.secretType">
Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets.
</Accordion>
<Accordion title="managedSecretReference.template">
Templates enable you to transform data from Infisical before storing it as a Kubernetes Secret.
</Accordion>
<Accordion title="managedSecretReference.template.includeAllSecrets">
When set to true, this option injects all secrets retrieved from Infisical into your configuration.
Secrets defined in the template will override the automatically injected secrets.
</Accordion>
<Accordion title="managedSecretReference.template.data">
Define secret keys and their corresponding templates.
Each data value uses a Golang template with access to all secrets retrieved from the specified scope.
Secrets are structured as follows:
```golang
type TemplateSecret struct {
Value string `json:"value"`
SecretPath string `json:"secretPath"`
}
```
#### Example template configuration:
```golang
managedSecretReference:
secretName: managed-secret
secretNamespace: default
template:
includeAllSecrets: true
data:
NEW_KEY: "{{ .KEY1.SecretPath }} {{ .KEY1.Value }}"
```
When you run the following command:
```bash
kubectl get secret managed-secret -o jsonpath='{.data}'
```
You'll receive Kubernetes secrets output that includes the NEW_KEY:
```bash
{... "KEY":"d29ybGQ=","NEW_KEY":"LyBoZWxsbw=="}
```
When you set `includeAllSecrets` as `false` the Kubernetes secrets outputs will be:
```bash
{"NEW_KEY":"LyBoZWxsbw=="}
```
</Accordion> </Accordion>
<Accordion title="managedSecretReference.creationPolicy"> <Accordion title="managedSecretReference.creationPolicy">
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
@@ -689,7 +568,104 @@ This is useful for tools such as ArgoCD, where every resource requires an owner
</Accordion> </Accordion>
### Apply the InfisicalSecret CRD to your cluster ### Manged secret templating
Fetching secrets from Infisical as is via the operator may not be enough. This is where templating functionality may be helpful.
Using Go templates, you can format, combine, and create new key-value pairs from secrets fetched from Infisical before storing them as Kubernetes Secrets.
<Accordion title="managedSecretReference.template">
</Accordion>
<Accordion title="managedSecretReference.template.includeAllSecrets">
This property controls what secrets are included in your managed secret when using templates.
When set to `true`, all secrets fetched from your Infisical project will be added into your managed Kubernetes secret resource.
**Use this option when you would like to sync all secrets from Infisical to Kubernetes but want to template a subset of them.**
When set to `false`, only secrets defined in the `managedSecretReference.template.data` field of the template will be included in the managed secret.
Use this option when you would like to sync **only** a subset of secrets from Infisical to Kubernetes.
</Accordion>
<Accordion title="managedSecretReference.template.data">
Define secret keys and their corresponding templates.
Each data value uses a Golang template with access to all secrets retrieved from the specified scope.
Secrets are structured as follows:
```golang
type TemplateSecret struct {
Value string `json:"value"`
SecretPath string `json:"secretPath"`
}
```
#### Example template configuration:
```yaml
managedSecretReference:
secretName: managed-secret
secretNamespace: default
template:
includeAllSecrets: true
data:
# Create new secret key that doesn't exist in your Infisical project using values of other secrets
NEW_KEY: "{{ .DB_PASSWORD.Value }}"
# Override an existing secret key in Infisical project with a new value using values of other secrets
API_URL: "https://api.{{.COMPANY_NAME.Value}}.{{.REGION.Value}}.com"
```
For this example, let's assume the following secrets exist in your Infisical project:
```
DB_PASSWORD = "secret123"
COMPANY_NAME = "acme"
REGION = "us-east-1"
API_URL = "old-url" # This will be overridden
```
The resulting managed Kubernetes secret will then contain:
```
# Original secrets (from includeAllSecrets: true)
DB_PASSWORD = "secret123"
COMPANY_NAME = "acme"
REGION = "us-east-1"
# New and overridden templated secrets
NEW_KEY = "secret123" # New secret created from template
API_URL = "https://api.acme.us-east-1.com" # Existing secret overridden by template
```
To help transform your secrets further, the operator provides a set of built-in functions that you can use in your templates.
### Available templating functions
<Accordion title="decodeBase64ToBytes">
**Function name**: decodeBase64ToBytes
**Description**:
Given a base64 encoded string, this function will decodes the base64-encoded string.
This function is useful when your secrets are already stored as base64 encoded value in Infisical.
**Returns**: The decoded base64 string as bytes.
**Example**:
The example below assumes that the `BINARY_KEY_BASE64` secret is stored as a base64 encoded value in Infisical.
The resulting managed secret will contain the decoded value of `BINARY_KEY_BASE64`.
```yaml
managedSecretReference:
secretName: managed-secret
secretNamespace: default
template:
includeAllSecrets: true
data:
BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}"
```
</Accordion>
</Accordion>
## Applying CRD
Once you have configured the InfisicalSecret CRD with the required fields, you can apply it to your cluster. Once you have configured the InfisicalSecret CRD with the required fields, you can apply it to your cluster.
After applying, you should notice that the managed secret has been created in the desired namespace your specified. After applying, you should notice that the managed secret has been created in the desired namespace your specified.
@@ -698,8 +674,6 @@ After applying, you should notice that the managed secret has been created in th
kubectl apply -f example-infisical-secret-crd.yaml kubectl apply -f example-infisical-secret-crd.yaml
``` ```
### Verify managed secret creation
To verify that the operator has successfully created the managed secret, you can check the secrets in the namespace that was specified. To verify that the operator has successfully created the managed secret, you can check the secrets in the namespace that was specified.
```bash ```bash
@@ -714,7 +688,7 @@ kubectl get secrets -n <namespace of managed secret>
## Using managed secret in your deployment ## Using managed secret in your deployment
Incorporating the managed secret created by the operator into your deployment can be achieved through several methods. To make use of the managed secret created by the operator into your deployment can be achieved through several methods.
Here, we will highlight three of the most common ways to utilize it. Learn more about Kubernetes secrets [here](https://kubernetes.io/docs/concepts/configuration/secret/) Here, we will highlight three of the most common ways to utilize it. Learn more about Kubernetes secrets [here](https://kubernetes.io/docs/concepts/configuration/secret/)
<Accordion title="envFrom"> <Accordion title="envFrom">
@@ -960,4 +934,4 @@ metadata:
type: Opaque type: Opaque
``` ```
</Accordion> </Accordion>
+2 -2
View File
@@ -301,8 +301,8 @@
"project-id-description2": "For more guidance, including code snipets for various languages and frameworks, see ", "project-id-description2": "For more guidance, including code snipets for various languages and frameworks, see ",
"auto-generated": "This is your project's auto-generated unique identifier. It can't be changed.", "auto-generated": "This is your project's auto-generated unique identifier. It can't be changed.",
"docs": "Infisical Docs", "docs": "Infisical Docs",
"auto-capitalization": "Auto Capitalization", "enforce-capitalization": "Enforce Capitalization",
"auto-capitalization-description": "According to standards, Infisical will automatically capitalize your keys. If you want to disable this feature, you can do so here." "enforce-capitalization-description": "According to standards, Infisical enforces uppercase secret keys. If you want to disable this feature, you can do so here."
} }
}, },
"signup": { "signup": {
+2 -2
View File
@@ -289,8 +289,8 @@
"project-id-description2": "Para más guías, incluyendo ejemplos de código en diferentes lenguajes y frameworks, visita ", "project-id-description2": "Para más guías, incluyendo ejemplos de código en diferentes lenguajes y frameworks, visita ",
"auto-generated": "Este es el ID único y autogenerado de proyecto. No se puede modificar.", "auto-generated": "Este es el ID único y autogenerado de proyecto. No se puede modificar.",
"docs": "Documentación de Infisical", "docs": "Documentación de Infisical",
"auto-capitalization": "Mayúsculas automáticas", "enforce-capitalization": "Hacer cumplir la capitalización",
"auto-capitalization-description": "De acuerdo con los estándares, Infisical pondrá en mayúsculas tus claves. Si quieres desactivar esta funcionalidad, lo puedes hacer aquí." "enforce-capitalization-description": "Según los estándares, Infisical aplica claves secretas en mayúsculas. Si desea desactivar esta función, puede hacerlo aquí."
} }
}, },
"signup": { "signup": {
@@ -266,8 +266,8 @@
"project-id-description2": "Para obter mais orientações, incluindo trechos de código para várias linguagens e frameworks, consulte ", "project-id-description2": "Para obter mais orientações, incluindo trechos de código para várias linguagens e frameworks, consulte ",
"auto-generated": "Este é o identificador exclusivo - gerado automaticamente - do seu projeto. Não pode ser alterado.", "auto-generated": "Este é o identificador exclusivo - gerado automaticamente - do seu projeto. Não pode ser alterado.",
"docs": "Documentação do Infisical", "docs": "Documentação do Infisical",
"auto-capitalization": "Converter em caixa alta automaticamente", "enforce-capitalization": "Aplicar capitalização",
"auto-capitalization-description": "Por padrão, Infisical converte automaticamente as chaves em caixa alta. Se você quiser desativar essa funcionalidade, pode fazê-lo aqui." "enforce-capitalization-description": "De acordo com os padrões, o Infisical impõe chaves secretas em letras maiúsculas. Se quiser desabilitar esse recurso, você pode fazer isso aqui."
} }
}, },
"signup": { "signup": {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,87 @@
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { useTimedReset } from "@app/hooks";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { createNotification } from "../notifications";
import { IconButton, Tooltip } from "../v2";
type Props = {
secretPathSegments: string[];
selectedPathSegmentIndex: number;
environmentSlug: string;
projectId: string;
};
export const SecretDashboardPathBreadcrumb = ({
secretPathSegments,
selectedPathSegmentIndex,
environmentSlug,
projectId
}: Props) => {
const [, isCopying, setIsCopying] = useTimedReset({
initialState: false
});
const newSecretPath = `/${secretPathSegments.slice(0, selectedPathSegmentIndex + 1).join("/")}`;
const isLastItem = secretPathSegments.length === selectedPathSegmentIndex + 1;
const folderName = secretPathSegments.at(selectedPathSegmentIndex);
return (
<div className="flex items-center space-x-3">
{isLastItem ? (
<div className="group flex items-center space-x-2">
<span
className={twMerge(
"text-sm font-semibold transition-all",
isCopying ? "text-bunker-200" : "text-bunker-300"
)}
>
{folderName}
</span>
<Tooltip className="relative right-2" position="bottom" content="Copy secret path">
<IconButton
variant="plain"
ariaLabel="copy"
onClick={() => {
if (isCopying) return;
setIsCopying(true);
navigator.clipboard.writeText(newSecretPath);
createNotification({
text: "Copied secret path to clipboard",
type: "info"
});
}}
className="opacity-0 transition duration-75 hover:bg-bunker-100/10 group-hover:opacity-100"
>
<FontAwesomeIcon
icon={!isCopying ? faCopy : faCheck}
size="sm"
className="cursor-pointer"
/>
</IconButton>
</Tooltip>
</div>
) : (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secrets/$envSlug` as const}
params={{
projectId,
envSlug: environmentSlug
}}
search={(query) => ({ ...query, secretPath: newSecretPath })}
className={twMerge(
"text-sm font-semibold transition-all hover:text-primary",
isCopying && "text-primary"
)}
>
{folderName}
</Link>
)}
</div>
);
};
@@ -0,0 +1,217 @@
/* eslint-disable react/prop-types */
import React from "react";
import { faCaretDown, faChevronRight, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, ReactNode } from "@tanstack/react-router";
import { LinkComponentProps } from "node_modules/@tanstack/react-router/dist/esm/link";
import { twMerge } from "tailwind-merge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger
} from "../Dropdown";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => (
<ol
ref={ref}
className={twMerge(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-bunker-100 sm:gap-2.5",
className
)}
{...props}
/>
)
);
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => (
<li
ref={ref}
className={twMerge("inline-flex items-center gap-1.5 font-medium", className)}
{...props}
/>
)
);
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<"div"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
return (
<div
ref={ref}
className={twMerge("transition-colors hover:text-primary-400", className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={twMerge("font-normal text-bunker-200 last:text-bunker-300", className)}
{...props}
/>
)
);
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={twMerge("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
{...props}
>
{children ?? <FontAwesomeIcon icon={faChevronRight} />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={twMerge("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<FontAwesomeIcon icon={faEllipsis} className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
enum BreadcrumbTypes {
Dropdown = "dropdown",
Component = "component"
}
export type TBreadcrumbFormat =
| {
type: BreadcrumbTypes.Dropdown;
label: string;
dropdownTitle?: string;
links: { label: string; link: LinkComponentProps }[];
}
| {
type: BreadcrumbTypes.Component;
component: ReactNode;
}
| {
type: undefined;
link?: LinkComponentProps;
label: string;
icon?: ReactNode;
};
const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[] }) => (
<div className="mx-auto max-w-7xl py-4 capitalize text-white">
<Breadcrumb>
<BreadcrumbList>
{(breadcrumbs as TBreadcrumbFormat[]).map((el, index) => {
const isNotLastCrumb = index + 1 !== breadcrumbs.length;
const BreadcrumbSegment = isNotLastCrumb ? BreadcrumbLink : BreadcrumbPage;
if (el.type === BreadcrumbTypes.Dropdown) {
return (
<React.Fragment key={`breadcrumb-group-${index + 1}`}>
<DropdownMenu>
<DropdownMenuTrigger>
<BreadcrumbItem>
<BreadcrumbSegment>
{el.label} <FontAwesomeIcon icon={faCaretDown} size="sm" />
</BreadcrumbSegment>
</BreadcrumbItem>
</DropdownMenuTrigger>
<DropdownMenuContent>
{el?.dropdownTitle && <DropdownMenuLabel>{el.dropdownTitle}</DropdownMenuLabel>}
{el.links.map((i, dropIndex) => (
<Link
{...i.link}
key={`breadcrumb-group-${index + 1}-dropdown-${dropIndex + 1}`}
>
<DropdownMenuItem>{i.label}</DropdownMenuItem>
</Link>
))}
</DropdownMenuContent>
</DropdownMenu>
{isNotLastCrumb && <BreadcrumbSeparator />}
</React.Fragment>
);
}
if (el.type === BreadcrumbTypes.Component) {
const Component = el.component;
return (
<React.Fragment key={`breadcrumb-group-${index + 1}`}>
<BreadcrumbItem>
<BreadcrumbSegment>
<Component />
</BreadcrumbSegment>
</BreadcrumbItem>
{isNotLastCrumb && <BreadcrumbSeparator />}
</React.Fragment>
);
}
const Icon = el?.icon;
return (
<React.Fragment key={`breadcrumb-group-${index + 1}`}>
{"link" in el && isNotLastCrumb ? (
<Link {...el.link}>
<BreadcrumbItem>
<BreadcrumbLink className="inline-flex items-center gap-1.5">
{Icon && <Icon />}
{el.label}
</BreadcrumbLink>
</BreadcrumbItem>
</Link>
) : (
<BreadcrumbItem>
<BreadcrumbPage className="inline-flex items-center gap-1.5">
{Icon && <Icon />}
{el.label}
</BreadcrumbPage>
</BreadcrumbItem>
)}
{isNotLastCrumb && <BreadcrumbSeparator />}
</React.Fragment>
);
})}
</BreadcrumbList>
</Breadcrumb>
</div>
);
export {
Breadcrumb,
BreadcrumbContainer,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbTypes
};
@@ -0,0 +1 @@
export * from "./Breadcrumb";
+36 -43
View File
@@ -1,6 +1,5 @@
import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react"; import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react";
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react"; import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export type MenuProps = { export type MenuProps = {
@@ -30,7 +29,7 @@ export const MenuItem = <T extends ElementType = "button">({
className, className,
isDisabled, isDisabled,
isSelected, isSelected,
as: Item = "button", as: Item = "div",
description, description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props // wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef, inputRef,
@@ -38,46 +37,40 @@ export const MenuItem = <T extends ElementType = "button">({
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => { }: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null); const iconRef = useRef<DotLottie | null>(null);
return ( return (
<div onMouseEnter={() => iconRef.current?.play()} onMouseLeave={() => iconRef.current?.stop()}> <Item
<li type="button"
className={twMerge( role="menuitem"
"duration-50 group mt-0.5 flex cursor-pointer flex-col rounded px-1 py-2 font-inter text-sm text-bunker-100 transition-all hover:bg-mineshaft-700", className={twMerge(
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600", "duration-50 group relative mt-0.5 flex w-full cursor-pointer items-center rounded px-1 py-2 font-inter text-sm text-bunker-100 transition-all hover:bg-mineshaft-700",
isDisabled && "cursor-not-allowed hover:bg-transparent", isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
className isDisabled && "cursor-not-allowed hover:bg-transparent",
)} className
> )}
<motion.span className="flex w-full flex-row items-center justify-start rounded-sm"> ref={inputRef}
<Item onMouseEnter={() => iconRef.current?.play()}
type="button" onMouseLeave={() => iconRef.current?.stop()}
role="menuitem" {...props}
className="relative flex items-center" >
ref={inputRef} <div
{...props} className={`${
> isSelected ? "visisble" : "invisible"
<div } absolute -left-[0.28rem] h-5 w-[0.07rem] rounded-md bg-primary`}
className={`${ />
isSelected ? "visisble" : "invisible" {icon && (
} absolute -left-[0.28rem] h-5 w-[0.07rem] rounded-md bg-primary`} <div style={{ width: "22px", height: "22px" }} className="my-auto ml-1 mr-3">
/> <DotLottieReact
{icon && ( dotLottieRefCallback={(el) => {
<div style={{ width: "22px", height: "22px" }} className="my-auto ml-1 mr-3"> iconRef.current = el;
<DotLottieReact }}
dotLottieRefCallback={(el) => { src={`/lotties/${icon}.json`}
iconRef.current = el; loop
}} className="h-full w-full"
src={`/lotties/${icon}.json`} />
loop </div>
className="h-full w-full" )}
/> <span className="flex-grow text-left">{children}</span>
</div> {description && <span className="mt-2 text-xs">{description}</span>}
)} </Item>
<span className="flex-grow text-left">{children}</span>
</Item>
{description && <span className="mt-2 text-xs">{description}</span>}
</motion.span>
</li>
</div>
); );
}; };
@@ -90,7 +83,7 @@ export type MenuGroupProps = {
export const MenuGroup = ({ children, title }: MenuGroupProps): JSX.Element => ( export const MenuGroup = ({ children, title }: MenuGroupProps): JSX.Element => (
<> <>
<li className="p-2 text-xs text-gray-400">{title}</li> <li className="px-2 pt-3 text-xs uppercase text-gray-400">{title}</li>
{children} {children}
</> </>
); );
@@ -0,0 +1,19 @@
import { ReactNode } from "@tanstack/react-router";
type Props = {
title: ReactNode;
description?: ReactNode;
children?: ReactNode;
};
export const PageHeader = ({ title, description, children }: Props) => (
<div className="mb-4">
<div className="flex w-full justify-between">
<div>
<h1 className="mr-4 text-3xl font-semibold capitalize text-white">{title}</h1>
</div>
<div>{children}</div>
</div>
<div className="mt-2 text-gray-400">{description}</div>
</div>
);
@@ -0,0 +1 @@
export { PageHeader } from "./PageHeader";
+2
View File
@@ -2,6 +2,7 @@
export * from "./Accordion"; export * from "./Accordion";
export * from "./Alert"; export * from "./Alert";
export * from "./Badge"; export * from "./Badge";
export * from "./Breadcrumb";
export * from "./Button"; export * from "./Button";
export * from "./Card"; export * from "./Card";
export * from "./Checkbox"; export * from "./Checkbox";
@@ -21,6 +22,7 @@ export * from "./Input";
export * from "./Menu"; export * from "./Menu";
export * from "./Modal"; export * from "./Modal";
export * from "./NoticeBanner"; export * from "./NoticeBanner";
export * from "./PageHeader";
export * from "./Pagination"; export * from "./Pagination";
export * from "./Popoverv2"; export * from "./Popoverv2";
export * from "./SecretInput"; export * from "./SecretInput";
+64 -64
View File
@@ -18,269 +18,269 @@ export const ROUTE_PATHS = Object.freeze({
Organization: { Organization: {
SecretScanning: setRoute( SecretScanning: setRoute(
"/organization/secret-scanning", "/organization/secret-scanning",
"/_authenticate/_inject-org-details/organization/_layout/secret-scanning" "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
), ),
SettingsPage: setRoute( SettingsPage: setRoute(
"/organization/settings", "/organization/settings",
"/_authenticate/_inject-org-details/organization/_layout/settings" "/_authenticate/_inject-org-details/_org-layout/organization/settings"
), ),
GroupDetailsByIDPage: setRoute( GroupDetailsByIDPage: setRoute(
"/organization/groups/$groupId", "/organization/groups/$groupId",
"/_authenticate/_inject-org-details/organization/_layout/groups/$groupId" "/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId"
), ),
IdentityDetailsByIDPage: setRoute( IdentityDetailsByIDPage: setRoute(
"/organization/identities/$identityId", "/organization/identities/$identityId",
"/_authenticate/_inject-org-details/organization/_layout/identities/$identityId" "/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId"
), ),
UserDetailsByIDPage: setRoute( UserDetailsByIDPage: setRoute(
"/organization/members/$membershipId", "/organization/members/$membershipId",
"/_authenticate/_inject-org-details/organization/_layout/members/$membershipId" "/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId"
), ),
AccessControlPage: setRoute( AccessControlPage: setRoute(
"/organization/access-management", "/organization/access-management",
"/_authenticate/_inject-org-details/organization/_layout/access-management" "/_authenticate/_inject-org-details/_org-layout/organization/access-management"
), ),
RoleByIDPage: setRoute( RoleByIDPage: setRoute(
"/organization/roles/$roleId", "/organization/roles/$roleId",
"/_authenticate/_inject-org-details/organization/_layout/roles/$roleId" "/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId"
), ),
AppConnections: { AppConnections: {
GithubOauthCallbackPage: setRoute( GithubOauthCallbackPage: setRoute(
"/organization/app-connections/github/oauth/callback", "/organization/app-connections/github/oauth/callback",
"/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback" "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback"
) )
} }
}, },
SecretManager: { SecretManager: {
ApprovalPage: setRoute( ApprovalPage: setRoute(
"/secret-manager/$projectId/approval", "/secret-manager/$projectId/approval",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/approval" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/approval"
), ),
SecretDashboardPage: setRoute( SecretDashboardPage: setRoute(
"/secret-manager/$projectId/secrets/$envSlug", "/secret-manager/$projectId/secrets/$envSlug",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug"
), ),
OverviewPage: setRoute( OverviewPage: setRoute(
"/secret-manager/$projectId/overview", "/secret-manager/$projectId/overview",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/overview" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview"
), ),
IntegrationsListPage: setRoute( IntegrationsListPage: setRoute(
"/secret-manager/$projectId/integrations", "/secret-manager/$projectId/integrations",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/"
), ),
IntegrationDetailsByIDPage: setRoute( IntegrationDetailsByIDPage: setRoute(
"/secret-manager/$projectId/integrations/$integrationId", "/secret-manager/$projectId/integrations/$integrationId",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId"
), ),
SecretSyncDetailsByIDPage: setRoute( SecretSyncDetailsByIDPage: setRoute(
"/secret-manager/$projectId/integrations/secret-syncs/$destination/$syncId", "/secret-manager/$projectId/integrations/secret-syncs/$destination/$syncId",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId"
), ),
Integratons: { Integratons: {
SelectIntegrationAuth: setRoute( SelectIntegrationAuth: setRoute(
"/secret-manager/$projectId/integrations/select-integration-auth", "/secret-manager/$projectId/integrations/select-integration-auth",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth"
), ),
HerokuOauthCallbackPage: setRoute( HerokuOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/heroku/oauth2/callback", "/secret-manager/$projectId/integrations/heroku/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback"
), ),
HerokuConfigurePage: setRoute( HerokuConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/heroku/create", "/secret-manager/$projectId/integrations/heroku/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/heroku/create"
), ),
AwsParameterStoreConfigurePage: setRoute( AwsParameterStoreConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/aws-parameter-store/create", "/secret-manager/$projectId/integrations/aws-parameter-store/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create"
), ),
AwsSecretManagerConfigurePage: setRoute( AwsSecretManagerConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/aws-secret-manager/create", "/secret-manager/$projectId/integrations/aws-secret-manager/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create"
), ),
AzureAppConfigurationsOauthCallbackPage: setRoute( AzureAppConfigurationsOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/azure-app-configuration/oauth2/callback", "/secret-manager/$projectId/integrations/azure-app-configuration/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
), ),
AzureAppConfigurationsConfigurePage: setRoute( AzureAppConfigurationsConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/azure-app-configuration/create", "/secret-manager/$projectId/integrations/azure-app-configuration/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create"
), ),
AzureDevopsConfigurePage: setRoute( AzureDevopsConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/azure-devops/create", "/secret-manager/$projectId/integrations/azure-devops/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-devops/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-devops/create"
), ),
AzureKeyVaultAuthorizePage: setRoute( AzureKeyVaultAuthorizePage: setRoute(
"/secret-manager/$projectId/integrations/azure-key-vault/authorize", "/secret-manager/$projectId/integrations/azure-key-vault/authorize",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize"
), ),
AzureKeyVaultOauthCallbackPage: setRoute( AzureKeyVaultOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/azure-key-vault/oauth2/callback", "/secret-manager/$projectId/integrations/azure-key-vault/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
), ),
AzureKeyVaultConfigurePage: setRoute( AzureKeyVaultConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/azure-key-vault/create", "/secret-manager/$projectId/integrations/azure-key-vault/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/azure-key-vault/create"
), ),
BitbucketOauthCallbackPage: setRoute( BitbucketOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/bitbucket/oauth2/callback", "/secret-manager/$projectId/integrations/bitbucket/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
), ),
BitbucketConfigurePage: setRoute( BitbucketConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/bitbucket/create", "/secret-manager/$projectId/integrations/bitbucket/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/bitbucket/create"
), ),
ChecklyConfigurePage: setRoute( ChecklyConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/checkly/create", "/secret-manager/$projectId/integrations/checkly/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/checkly/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/checkly/create"
), ),
CircleConfigurePage: setRoute( CircleConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/circleci/create", "/secret-manager/$projectId/integrations/circleci/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/circleci/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/circleci/create"
), ),
CloudflarePagesConfigurePage: setRoute( CloudflarePagesConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/cloudflare-pages/create", "/secret-manager/$projectId/integrations/cloudflare-pages/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create"
), ),
DigitalOceanAppPlatformConfigurePage: setRoute( DigitalOceanAppPlatformConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/digital-ocean-app-platform/create", "/secret-manager/$projectId/integrations/digital-ocean-app-platform/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create"
), ),
CloudflareWorkersConfigurePage: setRoute( CloudflareWorkersConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/cloudflare-workers/create", "/secret-manager/$projectId/integrations/cloudflare-workers/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create"
), ),
CodefreshConfigurePage: setRoute( CodefreshConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/codefresh/create", "/secret-manager/$projectId/integrations/codefresh/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/codefresh/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/codefresh/create"
), ),
GcpSecretManagerConfigurePage: setRoute( GcpSecretManagerConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/gcp-secret-manager/create", "/secret-manager/$projectId/integrations/gcp-secret-manager/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create"
), ),
GcpSecretManagerOauthCallbackPage: setRoute( GcpSecretManagerOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/gcp-secret-manager/oauth2/callback", "/secret-manager/$projectId/integrations/gcp-secret-manager/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback"
), ),
GithubConfigurePage: setRoute( GithubConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/github/create", "/secret-manager/$projectId/integrations/github/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/github/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/github/create"
), ),
GithubOauthCallbackPage: setRoute( GithubOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/github/oauth2/callback", "/secret-manager/$projectId/integrations/github/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/github/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/github/oauth2/callback"
), ),
GitlabConfigurePage: setRoute( GitlabConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/gitlab/create", "/secret-manager/$projectId/integrations/gitlab/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/create"
), ),
GitlabOauthCallbackPage: setRoute( GitlabOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/gitlab/oauth2/callback", "/secret-manager/$projectId/integrations/gitlab/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback"
), ),
VercelOauthCallbackPage: setRoute( VercelOauthCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/vercel/oauth2/callback", "/secret-manager/$projectId/integrations/vercel/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback"
), ),
VercelConfigurePage: setRoute( VercelConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/vercel/create", "/secret-manager/$projectId/integrations/vercel/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/vercel/create"
), ),
FlyioConfigurePage: setRoute( FlyioConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/flyio/create", "/secret-manager/$projectId/integrations/flyio/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/flyio/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/flyio/create"
), ),
HashicorpVaultConfigurePage: setRoute( HashicorpVaultConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/hashicorp-vault/create", "/secret-manager/$projectId/integrations/hashicorp-vault/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create"
), ),
HasuraCloudConfigurePage: setRoute( HasuraCloudConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/hasura-cloud/create", "/secret-manager/$projectId/integrations/hasura-cloud/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/hasura-cloud/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/hasura-cloud/create"
), ),
LaravelForgeConfigurePage: setRoute( LaravelForgeConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/laravel-forge/create", "/secret-manager/$projectId/integrations/laravel-forge/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/laravel-forge/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/laravel-forge/create"
), ),
NorthflankConfigurePage: setRoute( NorthflankConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/northflank/create", "/secret-manager/$projectId/integrations/northflank/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/northflank/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/northflank/create"
), ),
RailwayConfigurePage: setRoute( RailwayConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/railway/create", "/secret-manager/$projectId/integrations/railway/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/railway/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/railway/create"
), ),
RenderConfigurePage: setRoute( RenderConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/render/create", "/secret-manager/$projectId/integrations/render/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/render/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/render/create"
), ),
RundeckConfigurePage: setRoute( RundeckConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/rundeck/create", "/secret-manager/$projectId/integrations/rundeck/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/rundeck/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/rundeck/create"
), ),
WindmillConfigurePage: setRoute( WindmillConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/windmill/create", "/secret-manager/$projectId/integrations/windmill/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/windmill/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/windmill/create"
), ),
TravisCIConfigurePage: setRoute( TravisCIConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/travisci/create", "/secret-manager/$projectId/integrations/travisci/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/travisci/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/travisci/create"
), ),
TerraformCloudConfigurePage: setRoute( TerraformCloudConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/terraform-cloud/create", "/secret-manager/$projectId/integrations/terraform-cloud/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/terraform-cloud/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/terraform-cloud/create"
), ),
TeamcityConfigurePage: setRoute( TeamcityConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/teamcity/create", "/secret-manager/$projectId/integrations/teamcity/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/teamcity/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/teamcity/create"
), ),
SupabaseConfigurePage: setRoute( SupabaseConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/supabase/create", "/secret-manager/$projectId/integrations/supabase/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/supabase/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/supabase/create"
), ),
OctopusDeployCloudConfigurePage: setRoute( OctopusDeployCloudConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/octopus-deploy/create", "/secret-manager/$projectId/integrations/octopus-deploy/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/octopus-deploy/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/octopus-deploy/create"
), ),
DatabricksConfigurePage: setRoute( DatabricksConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/databricks/create", "/secret-manager/$projectId/integrations/databricks/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/databricks/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/databricks/create"
), ),
QoveryConfigurePage: setRoute( QoveryConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/qovery/create", "/secret-manager/$projectId/integrations/qovery/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/qovery/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/qovery/create"
), ),
Cloud66ConfigurePage: setRoute( Cloud66ConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/cloud-66/create", "/secret-manager/$projectId/integrations/cloud-66/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/cloud-66/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/cloud-66/create"
), ),
NetlifyConfigurePage: setRoute( NetlifyConfigurePage: setRoute(
"/secret-manager/$projectId/integrations/netlify/create", "/secret-manager/$projectId/integrations/netlify/create",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/create" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/create"
), ),
NetlifyOuathCallbackPage: setRoute( NetlifyOuathCallbackPage: setRoute(
"/secret-manager/$projectId/integrations/netlify/oauth2/callback", "/secret-manager/$projectId/integrations/netlify/oauth2/callback",
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback" "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback"
) )
} }
}, },
CertManager: { CertManager: {
CertAuthDetailsByIDPage: setRoute( CertAuthDetailsByIDPage: setRoute(
"/cert-manager/$projectId/ca/$caId", "/cert-manager/$projectId/ca/$caId",
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId"
), ),
OverviewPage: setRoute( OverviewPage: setRoute(
"/cert-manager/$projectId/overview", "/cert-manager/$projectId/overview",
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/overview" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview"
), ),
PkiCollectionDetailsByIDPage: setRoute( PkiCollectionDetailsByIDPage: setRoute(
"/cert-manager/$projectId/pki-collections/$collectionId", "/cert-manager/$projectId/pki-collections/$collectionId",
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
) )
}, },
Ssh: { Ssh: {
SshCaByIDPage: setRoute( SshCaByIDPage: setRoute(
"/ssh/$projectId/ca/$caId", "/ssh/$projectId/ca/$caId",
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/ca/$caId" "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId"
) )
}, },
Public: { Public: {
+15
View File
@@ -156,3 +156,18 @@ export const useCreateNewTotpRecoveryCodes = () => {
} }
}); });
}; };
export const useResendOrgMemberInvitation = () => {
return useMutation({
mutationFn: async (dto: { membershipId: string }) => {
const { data } = await apiRequest.post<{
signupToken?: {
email: string;
link: string;
};
}>("/api/v1/invite-org/signup-resend", dto);
return data.signupToken;
}
});
};
+2 -1
View File
@@ -30,8 +30,9 @@ export const useToggle = (initialState = false): UseToggleReturn => {
const timedToggle = useCallback((timeout = 2000) => { const timedToggle = useCallback((timeout = 2000) => {
setValue((prev) => !prev); setValue((prev) => !prev);
setTimeout(() => { const timeoutRef = setTimeout(() => {
setValue(false); setValue(false);
clearTimeout(timeoutRef);
}, timeout); }, timeout);
}, []); }, []);
@@ -14,7 +14,7 @@ import { envConfig } from "@app/config/env";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner"; import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/SidebarFooter/SidebarFooter"; import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar";
export const AdminLayout = () => { export const AdminLayout = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -1,174 +1,129 @@
import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons"; import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query"; import { Link, linkOptions, Outlet, useLocation, useRouterState } from "@tanstack/react-router";
import { Link, Outlet, useNavigate, useRouter } from "@tanstack/react-router"; import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Mfa } from "@app/components/auth/Mfa";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import SecurityClient from "@app/components/utilities/SecurityClient"; import {
import { Menu, MenuItem } from "@app/components/v2"; BreadcrumbContainer,
import { useUser } from "@app/context"; Menu,
import { usePopUp, useToggle } from "@app/hooks"; MenuGroup,
import { useSelectOrganization, workspaceKeys } from "@app/hooks/api"; MenuItem,
import { authKeys } from "@app/hooks/api/auth/queries"; TBreadcrumbFormat
import { MfaMethod } from "@app/hooks/api/auth/types"; } from "@app/components/v2";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { usePopUp } from "@app/hooks";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner"; import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
import { SidebarFooter } from "./components/SidebarFooter"; import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar";
import { SidebarHeader } from "./components/SidebarHeader"; import { SidebarHeader } from "./components/SidebarHeader";
export const OrganizationLayout = () => { export const OrganizationLayout = () => {
const [shouldShowMfa, toggleShowMfa] = useToggle(false); const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL); const location = useLocation();
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const isOrganizationSpecificPage = location.pathname.startsWith("/organization");
const breadcrumbs =
const { user } = useUser(); isOrganizationSpecificPage && matches && "breadcrumbs" in matches
? matches.breadcrumbs
: undefined;
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const); const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
const { mutateAsync: selectOrganization } = useSelectOrganization();
const navigate = useNavigate();
const router = useRouter();
const queryClient = useQueryClient();
const { t } = useTranslation(); const { t } = useTranslation();
const handleOrgChange = async (orgId: string) => {
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ const shouldShowOrgSidebar =
organizationId: orgId location.pathname.startsWith("/organization") &&
}); !(
[
if (isMfaEnabled) { linkOptions({ to: "/organization/secret-manager/overview" }).to,
SecurityClient.setMfaToken(token); linkOptions({ to: "/organization/cert-manager/overview" }).to,
if (mfaMethod) { linkOptions({ to: "/organization/ssh/overview" }).to,
setRequiredMfaMethod(mfaMethod); linkOptions({ to: "/organization/kms/overview" }).to,
} linkOptions({ to: "/organization/secret-scanning" }).to,
toggleShowMfa.on(); linkOptions({ to: "/organization/secret-sharing" }).to
setMfaSuccessCallback(() => () => handleOrgChange(orgId)); ] as string[]
return; ).includes(location.pathname);
}
await router.invalidate();
await navigateUserToOrg(navigate, orgId);
};
if (shouldShowMfa) {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Mfa
email={user.email as string}
method={requiredMfaMethod}
successCallback={mfaSuccessCallback}
closeMfa={() => toggleShowMfa.off()}
/>
</div>
);
}
return ( return (
<> <>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex"> <div className="dark hidden h-screen w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex">
{!window.isSecureContext && <InsecureConnectionBanner />} {!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row"> <div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"> <MinimizedOrgSidebar />
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]"> <AnimatePresence mode="popLayout">
<div> {shouldShowOrgSidebar && (
<SidebarHeader onChangeOrg={handleOrgChange} /> <motion.div
<div className="px-1"> key="menu-list-items"
<Menu className="mt-4"> initial={{ x: -150 }}
<Link to={`/organization/${ProjectType.SecretManager}/overview` as const}> animate={{ x: 0 }}
{({ isActive }) => ( exit={{ x: -150 }}
<MenuItem isSelected={isActive} icon="sliding-carousel"> transition={{ duration: 0.2 }}
Secret Management className="dark w-60 overflow-hidden border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900"
</MenuItem> >
)} <nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
</Link> <div className="p-2 pt-3">
<Link to={`/organization/${ProjectType.CertificateManager}/overview` as const}> <SidebarHeader />
{({ isActive }) => ( </div>
<MenuItem isSelected={isActive} icon="note"> <Menu>
Cert Management <MenuGroup title="Organization Control">
</MenuItem> <Link to="/organization/audit-logs">
)}
</Link>
<Link to={`/organization/${ProjectType.KMS}/overview` as const}>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="unlock">
Key Management
</MenuItem>
)}
</Link>
<Link to={`/organization/${ProjectType.SSH}/overview` as const}>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="verified">
SSH
</MenuItem>
)}
</Link>
<Link to="/organization/access-management">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="groups">
Access Control
</MenuItem>
)}
</Link>
<Link to="/organization/secret-scanning">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="secret-scan" className="text-white">
Secret Scanning
</MenuItem>
)}
</Link>
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Secret Sharing
</MenuItem>
)}
</Link>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (
<Link to="/organization/billing">
{({ isActive }) => ( {({ isActive }) => (
<MenuItem isSelected={isActive} icon="spinning-coin"> <MenuItem isSelected={isActive} icon="moving-block">
Usage & Billing Audit Logs
</MenuItem> </MenuItem>
)} )}
</Link> </Link>
)} {(window.location.origin.includes("https://app.infisical.com") ||
<Link to="/organization/audit-logs"> window.location.origin.includes("https://eu.infisical.com") ||
{({ isActive }) => ( window.location.origin.includes("https://gamma.infisical.com")) && (
<MenuItem isSelected={isActive} icon="moving-block"> <Link to="/organization/billing">
Audit Logs {({ isActive }) => (
</MenuItem> <MenuItem isSelected={isActive} icon="spinning-coin">
Usage & Billing
</MenuItem>
)}
</Link>
)} )}
</Link> </MenuGroup>
<Link to="/organization/settings"> <MenuGroup title="Other">
{({ isActive }) => ( <Link to="/organization/access-management">
<MenuItem isSelected={isActive} icon="toggle-settings"> {({ isActive }) => (
Organization Settings <MenuItem isSelected={isActive} icon="groups">
</MenuItem> Access Control
)} </MenuItem>
</Link> )}
</Link>
<Link to="/organization/settings">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
Organization Settings
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu> </Menu>
</div> </nav>
</div> </motion.div>
<SidebarFooter /> )}
</nav> </AnimatePresence>
</aside> <main
<CreateOrgModal className={twMerge(
isOpen={popUp?.createOrg?.isOpen} "flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]",
onClose={() => handlePopUpToggle("createOrg", false)} !isOrganizationSpecificPage && "overflow-hidden p-0"
/> )}
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]"> >
{breadcrumbs ? (
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
) : null}
<Outlet /> <Outlet />
</main> </main>
</div> </div>
</div> </div>
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden"> <div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" /> <FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
<p className="max-w-sm px-6 text-center text-lg text-gray-200"> <p className="max-w-sm px-6 text-center text-lg text-gray-200">
@@ -0,0 +1,63 @@
import { ComponentPropsWithRef, ElementType, useRef } from "react";
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
import { twMerge } from "tailwind-merge";
import { MenuItemProps } from "@app/components/v2";
export const MenuIconButton = <T extends ElementType = "button">({
children,
icon,
className,
isDisabled,
isSelected,
as: Item = "div",
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef,
lottieIconMode = "forward",
...props
}: MenuItemProps<T> &
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null);
return (
<Item
type="button"
role="menuitem"
className={twMerge(
"group relative flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
isSelected && "bg-bunker-800 hover:bg-mineshaft-600",
isDisabled && "cursor-not-allowed hover:bg-transparent",
className
)}
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
ref={inputRef}
{...props}
>
<div
className={`${
isSelected ? "opacity-100" : "opacity-0"
} absolute -left-[0.28rem] h-full w-1 rounded-md bg-primary transition-all duration-150`}
/>
{icon && (
<div className="my-auto mb-2 h-6 w-6">
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
mode={lottieIconMode}
/>
</div>
)}
<div
className="flex-grow justify-center break-words text-center"
style={{ fontSize: "10px" }}
>
{children}
</div>
</Item>
);
};
@@ -0,0 +1 @@
export { MenuIconButton } from "./MenuIconButton";
@@ -0,0 +1,488 @@
import { useState } from "react";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import {
faArrowUpRightFromSquare,
faBook,
faCheck,
faCog,
faEnvelope,
faInfinity,
faInfo,
faInfoCircle,
faMoneyBill,
faSignOut,
faUser,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { Link, linkOptions, useLocation, useNavigate, useRouter } from "@tanstack/react-router";
import { Mfa } from "@app/components/auth/Mfa";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import SecurityClient from "@app/components/utilities/SecurityClient";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import {
useGetOrganizations,
useGetOrgTrialUrl,
useLogoutUser,
useSelectOrganization,
workspaceKeys
} from "@app/hooks/api";
import { authKeys } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { SubscriptionPlan } from "@app/hooks/api/types";
import { AuthMethod } from "@app/hooks/api/users/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { MenuIconButton } from "../MenuIconButton";
const getPlan = (subscription: SubscriptionPlan) => {
if (subscription.dynamicSecret) return "Enterprise Plan";
if (subscription.pitRecovery) return "Pro Plan";
return "Free Plan";
};
export const INFISICAL_SUPPORT_OPTIONS = [
[
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
"Support Forum",
"https://infisical.com/slack"
],
[
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
"Read Docs",
"https://infisical.com/docs/documentation/getting-started/introduction"
],
[
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
"GitHub Issues",
"https://github.com/Infisical/infisical/issues"
],
[
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
"Email Support",
"mailto:[email protected]"
]
];
export const MinimizedOrgSidebar = () => {
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const { subscription } = useSubscription();
const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl();
const { currentOrg } = useOrganization();
const { data: orgs } = useGetOrganizations();
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
const { mutateAsync: selectOrganization } = useSelectOrganization();
const navigate = useNavigate();
const router = useRouter();
const location = useLocation();
const queryClient = useQueryClient();
const isMoreSelected = (
[
linkOptions({ to: "/organization/access-management" }).to,
linkOptions({ to: "/organization/settings" }).to,
linkOptions({ to: "/organization/audit-logs" }).to
] as string[]
).includes(location.pathname);
const handleOrgChange = async (orgId: string) => {
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
organizationId: orgId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
return;
}
await router.invalidate();
await navigateUserToOrg(navigate, orgId);
};
const logout = useLogoutUser();
const logOutUser = async () => {
try {
console.log("Logging out...");
await logout.mutateAsync();
navigate({ to: "/login" });
} catch (error) {
console.error(error);
}
};
if (shouldShowMfa) {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Mfa
email={user.email as string}
method={requiredMfaMethod}
successCallback={mfaSuccessCallback}
closeMfa={() => toggleShowMfa.off()}
/>
</div>
);
}
return (
<>
<aside
className="dark z-10 border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 transition-all duration-150"
style={{ width: "72px" }}
>
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div>
<div className="flex cursor-pointer items-center p-2 pt-4 hover:bg-mineshaft-700">
<DropdownMenu modal>
<DropdownMenuTrigger asChild>
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-1 transition-all">
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary">
{currentOrg?.name.charAt(0)}
</div>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="right"
className="p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "320px" }}
>
<div className="px-2 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
<div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary text-black">
{currentOrg?.name.charAt(0)}
</div>
<div className="flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
{currentOrg?.name}
</div>
<div className="text-xs text-mineshaft-400">{getPlan(subscription)}</div>
</div>
</div>
</div>
<div className="px-2 py-1 text-xs capitalize text-mineshaft-400">
organizations
</div>
{orgs?.map((org) => {
return (
<DropdownMenuItem key={org.id}>
<Button
onClick={async () => {
if (currentOrg?.id === org.id) return;
if (org.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
if (org.orgAuthMethod === AuthMethod.OIDC) {
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
} else {
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
}
window.close();
return;
}
handleOrgChange(org?.id);
}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
{org.name}
</div>
</Button>
</DropdownMenuItem>
);
})}
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faSignOut} />}
onClick={logOutUser}
>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="space-y-1 px-1">
<Link to="/organization/secret-manager/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(`/${ProjectType.SecretManager}`)
}
icon="sliding-carousel"
>
Secret Manager
</MenuIconButton>
)}
</Link>
<Link to="/organization/cert-manager/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(`/${ProjectType.CertificateManager}`)
}
icon="note"
>
Cert Manager
</MenuIconButton>
)}
</Link>
<Link to="/organization/kms/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive || window.location.pathname.startsWith(`/${ProjectType.KMS}`)
}
icon="unlock"
>
KMS
</MenuIconButton>
)}
</Link>
<Link to="/organization/ssh/overview">
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive || window.location.pathname.startsWith(`/${ProjectType.SSH}`)
}
icon="verified"
>
SSH
</MenuIconButton>
)}
</Link>
<div className="w-full bg-mineshaft-500" style={{ height: "1px" }} />
<Link to="/organization/secret-scanning">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="secret-scan">
Secret Scanning
</MenuIconButton>
)}
</Link>
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="lock-closed">
Secret Sharing
</MenuIconButton>
)}
</Link>
<div className="my-1 w-full bg-mineshaft-500" style={{ height: "1px" }} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="w-full">
<MenuIconButton
lottieIconMode="reverse"
icon="three-ellipsis"
isSelected={isMoreSelected}
>
More
</MenuIconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" side="right" className="p-1">
<DropdownMenuLabel>Organization Options</DropdownMenuLabel>
<Link to="/organization/access-management">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUsers} />}>
Access Control
</DropdownMenuItem>
</Link>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (
<Link to="/organization/billing">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faMoneyBill} />}>
Usage & Billing
</DropdownMenuItem>
</Link>
)}
<Link to="/organization/audit-logs">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faBook} />}>
Audit Logs
</DropdownMenuItem>
</Link>
<Link to="/organization/settings">
<DropdownMenuItem icon={<FontAwesomeIcon icon={faCog} />}>
Organization Settings
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div
className={`relative mt-10 ${
subscription && subscription.slug === "starter" && !subscription.has_used_trial
? "mb-2"
: "mb-4"
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
>
<DropdownMenu>
<DropdownMenuTrigger className="w-full">
<MenuIconButton>
<FontAwesomeIcon icon={faInfoCircle} className="mb-3 text-lg" />
Support
</MenuIconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
<DropdownMenuItem key={url as string}>
<a
target="_blank"
rel="noopener noreferrer"
href={String(url)}
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
</DropdownMenuItem>
))}
{envConfig.PLATFORM_VERSION && (
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
Version: {envConfig.PLATFORM_VERSION}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
<button
type="button"
onClick={async () => {
if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg.id,
success_url: window.location.href
});
window.location.href = url;
}}
className="mt-1.5 w-full"
>
<div className="justify-left mb-1.5 mt-1.5 flex w-full items-center rounded-md bg-mineshaft-600 py-1 pl-4 text-mineshaft-300 duration-200 hover:bg-mineshaft-500 hover:text-primary-400">
<FontAwesomeIcon icon={faInfinity} className="ml-0.5 mr-3 py-2 text-primary" />
Start Free Pro Trial
</div>
</button>
)}
<DropdownMenu>
<DropdownMenuTrigger className="w-full" asChild>
<div>
<MenuIconButton icon="user">User</MenuIconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
<div className="p-2">
<FontAwesomeIcon icon={faUser} className="text-mineshaft-400" />
</div>
<div className="flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
{user?.firstName} {user?.lastName}
</div>
<div className="text-xs text-mineshaft-300">{user.email}</div>
</div>
</div>
</div>
<Link to="/personal-settings">
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
</Link>
<a
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Documentation
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
<a
href="https://infisical.com/slack"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Join Slack Community
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
{user?.superAdmin && (
<Link to="/admin">
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Server Admin Console
</DropdownMenuItem>
</Link>
)}
<Link to="/organization/admin">
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Organization Admin Console
</DropdownMenuItem>
</Link>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</nav>
</aside>
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
</>
);
};
@@ -0,0 +1 @@
export { MinimizedOrgSidebar } from "./MinimizedOrgSidebar";
@@ -1,130 +0,0 @@
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import {
faBook,
faEnvelope,
faInfinity,
faInfo,
faPlus,
faQuestion
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { WishForm } from "@app/components/features/WishForm";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription } from "@app/context";
import { useGetOrgTrialUrl } from "@app/hooks/api";
export const INFISICAL_SUPPORT_OPTIONS = [
[
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
"Support Forum",
"https://infisical.com/slack"
],
[
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
"Read Docs",
"https://infisical.com/docs/documentation/getting-started/introduction"
],
[
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
"GitHub Issues",
"https://github.com/Infisical/infisical/issues"
],
[
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
"Email Support",
"mailto:[email protected]"
]
];
export const SidebarFooter = () => {
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const { mutateAsync } = useGetOrgTrialUrl();
return (
<div
className={`relative mt-10 ${
subscription && subscription.slug === "starter" && !subscription.has_used_trial
? "mb-2"
: "mb-4"
} flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400`}
>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && <WishForm />}
<Link
to="/organization/access-management"
search={{
action: "invite"
}}
className="w-full"
>
<div className="mb-3 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faPlus} className="mr-3" />
Invite people
</div>
</Link>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
Help & Support
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
<DropdownMenuItem key={url as string}>
<a
target="_blank"
rel="noopener noreferrer"
href={String(url)}
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
</DropdownMenuItem>
))}
{envConfig.PLATFORM_VERSION && (
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
Version: {envConfig.PLATFORM_VERSION}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
<button
type="button"
onClick={async () => {
if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg.id,
success_url: window.location.href
});
window.location.href = url;
}}
className="mt-1.5 w-full"
>
<div className="justify-left mb-1.5 mt-1.5 flex w-full items-center rounded-md bg-mineshaft-600 py-1 pl-4 text-mineshaft-300 duration-200 hover:bg-mineshaft-500 hover:text-primary-400">
<FontAwesomeIcon icon={faInfinity} className="ml-0.5 mr-3 py-2 text-primary" />
Start Free Pro Trial
</div>
</button>
)}
</div>
);
};
@@ -1 +0,0 @@
export { SidebarFooter } from "./SidebarFooter";
@@ -1,169 +1,27 @@
import { faAngleDown, faArrowUpRightFromSquare, faCheck } from "@fortawesome/free-solid-svg-icons"; import { useOrganization, useSubscription } from "@app/context";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SubscriptionPlan } from "@app/hooks/api/types";
import { Link, useNavigate } from "@tanstack/react-router";
import { const getPlan = (subscription: SubscriptionPlan) => {
Button, if (subscription.dynamicSecret) return "Enterprise Plan";
DropdownMenu, if (subscription.pitRecovery) return "Pro Plan";
DropdownMenuContent, return "Free Plan";
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2";
import { useOrganization, useUser } from "@app/context";
import { useGetOrganizations, useLogoutUser } from "@app/hooks/api";
import { AuthMethod } from "@app/hooks/api/users/types";
type Prop = {
onChangeOrg: (orgId: string) => void;
}; };
export const SidebarHeader = ({ onChangeOrg }: Prop) => { export const SidebarHeader = () => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { user } = useUser(); const { subscription } = useSubscription();
const navigate = useNavigate();
const { data: orgs } = useGetOrganizations();
const logout = useLogoutUser();
const logOutUser = async () => {
try {
console.log("Logging out...");
await logout.mutateAsync();
navigate({ to: "/login" });
} catch (error) {
console.error(error);
}
};
return ( return (
<div className="flex h-12 cursor-default items-center px-3 pt-6"> <div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
<DropdownMenu> <div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary">
<DropdownMenuTrigger asChild className="max-w-[160px] data-[state=open]:bg-mineshaft-600"> {currentOrg?.name.charAt(0)}
<div className="mr-auto flex items-center rounded-md py-1.5 pl-1.5 pr-2 hover:bg-mineshaft-600"> </div>
<div className="flex h-5 w-5 min-w-[20px] items-center justify-center rounded-md bg-primary text-sm"> <div className="flex flex-grow flex-col text-white">
{currentOrg?.name.charAt(0)} <div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
</div> {currentOrg?.name}
<div </div>
className="overflow-hidden truncate text-ellipsis pl-2 text-sm text-mineshaft-100" <div className="text-xs text-mineshaft-400">{getPlan(subscription)}</div>
style={{ maxWidth: "140px" }} </div>
>
{currentOrg?.name}
</div>
<FontAwesomeIcon icon={faAngleDown} className="pl-1 pt-1 text-xs text-mineshaft-300" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
{orgs?.map((org) => {
return (
<DropdownMenuItem key={org.id}>
<Button
onClick={async () => {
if (currentOrg?.id === org.id) return;
if (org.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
if (org.orgAuthMethod === AuthMethod.OIDC) {
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
} else {
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
}
window.close();
return;
}
onChangeOrg(org?.id);
}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
{org.name}
</div>
</Button>
</DropdownMenuItem>
);
})}
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<button type="button" onClick={logOutUser} className="w-full">
<DropdownMenuItem>Log Out</DropdownMenuItem>
</button>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger
asChild
className="p-1 hover:bg-primary-400 hover:text-black data-[state=open]:bg-primary-400 data-[state=open]:text-black"
>
<div
className="child flex items-center justify-center rounded-full bg-mineshaft pr-1 text-mineshaft-300 hover:bg-mineshaft-500"
style={{ fontSize: "11px", width: "26px", height: "26px" }}
>
{user?.firstName?.charAt(0)}
{user?.lastName && user?.lastName?.charAt(0)}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.username}</div>
<Link to="/personal-settings">
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
</Link>
<a
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Documentation
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
<a
href="https://infisical.com/slack"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Join Slack Community
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
{user?.superAdmin && (
<Link to="/admin">
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Server Admin Console
</DropdownMenuItem>
</Link>
)}
<Link to="/organization/admin">
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Organization Admin Console
</DropdownMenuItem>
</Link>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<button type="button" onClick={logOutUser} className="w-full">
<DropdownMenuItem>Log Out</DropdownMenuItem>
</button>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
); );
}; };
@@ -14,7 +14,7 @@ import { envConfig } from "@app/config/env";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner"; import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/SidebarFooter/SidebarFooter"; import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar";
export const PersonalSettingsLayout = () => { export const PersonalSettingsLayout = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -1,48 +1,30 @@
import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons"; import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query"; import { Link, Outlet, useRouterState } from "@tanstack/react-router";
import { Link, Outlet, useNavigate, useRouter } from "@tanstack/react-router"; import { motion } from "framer-motion";
import { Mfa } from "@app/components/auth/Mfa";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Menu, MenuItem } from "@app/components/v2";
import { useUser, useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
import { import {
useGetAccessRequestsCount, BreadcrumbContainer,
useGetSecretApprovalRequestCount, Menu,
useSelectOrganization, MenuGroup,
workspaceKeys MenuItem,
} from "@app/hooks/api"; TBreadcrumbFormat
import { authKeys } from "@app/hooks/api/auth/queries"; } from "@app/components/v2";
import { MfaMethod } from "@app/hooks/api/auth/types"; import { useWorkspace } from "@app/context";
import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
import { SidebarFooter } from "../OrganizationLayout/components/SidebarFooter";
import { ProjectSelect } from "./components/ProjectSelect"; import { ProjectSelect } from "./components/ProjectSelect";
import { SidebarHeader } from "./components/SidebarHeader";
// This is a generic layout shared by all types of projects. // This is a generic layout shared by all types of projects.
// If the product layout differs significantly, create a new layout as needed. // If the product layout differs significantly, create a new layout as needed.
export const ProjectLayout = () => { export const ProjectLayout = () => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const navigate = useNavigate(); const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const { user } = useUser();
const { mutateAsync: selectOrganization } = useSelectOrganization();
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter();
const queryClient = useQueryClient();
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || ""; const projectSlug = currentWorkspace?.slug || "";
@@ -63,191 +45,168 @@ export const ProjectLayout = () => {
const pendingRequestsCount = const pendingRequestsCount =
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0); (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
const handleOrgChange = async (orgId: string) => {
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
organizationId: orgId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
return;
}
await router.invalidate();
await navigateUserToOrg(navigate, orgId);
};
if (shouldShowMfa) {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Mfa
email={user.email as string}
method={requiredMfaMethod}
successCallback={mfaSuccessCallback}
closeMfa={() => toggleShowMfa.off()}
/>
</div>
);
}
return ( return (
<> <>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex"> <div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
{!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row"> <div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"> <motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]"> <nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div> <div>
<SidebarHeader onChangeOrg={handleOrgChange} />
<ProjectSelect /> <ProjectSelect />
<div className="px-1"> <div className="px-1">
<Menu> <Menu>
{isSecretManager && ( <MenuGroup title="Main Menu">
<Link {isSecretManager && (
to={`/${ProjectType.SecretManager}/$projectId/overview` as const} <Link
params={{ to={`/${ProjectType.SecretManager}/$projectId/overview` as const}
projectId: currentWorkspace.id params={{
}} projectId: currentWorkspace.id
> }}
{({ isActive }) => ( >
<MenuItem isSelected={isActive} icon="lock-closed"> {({ isActive }) => (
{t("nav.menu.secrets")} <MenuItem isSelected={isActive} icon="lock-closed">
</MenuItem> {t("nav.menu.secrets")}
)} </MenuItem>
</Link> )}
)} </Link>
{isCertManager && (
<Link
to={`/${ProjectType.CertificateManager}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
{isCmek && (
<Link
to={`/${ProjectType.KMS}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
{isSSH && (
<Link
to={`/${ProjectType.SSH}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
<Link
to={`/${currentWorkspace.type}/$projectId/access-management` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="groups">
Access Control
</MenuItem>
)} )}
</Link> {isCertManager && (
{isSecretManager && ( <Link
<Link to={`/${ProjectType.CertificateManager}/$projectId/overview` as const}
to={`/${ProjectType.SecretManager}/$projectId/integrations` as const} params={{
params={{ projectId: currentWorkspace.id
projectId: currentWorkspace.id }}
}} >
> {({ isActive }) => (
{({ isActive }) => ( <MenuItem isSelected={isActive} icon="lock-closed">
<MenuItem isSelected={isActive} icon="jigsaw-puzzle"> Overview
{t("nav.menu.integrations")} </MenuItem>
</MenuItem> )}
)} </Link>
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secret-rotation` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="rotation">
Secret Rotation
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/approval` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="circular-check">
Approvals
{Boolean(
secretApprovalReqCount?.open ||
accessApprovalRequestCount?.pendingCount
) && (
<span className="ml-2 rounded border border-primary-400 bg-primary-600 px-1 py-0.5 text-xs font-semibold text-black">
{pendingRequestsCount}
</span>
)}
</MenuItem>
)}
</Link>
)}
<Link
to={`/${currentWorkspace.type}/$projectId/settings` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
{t("nav.menu.project-settings")}
</MenuItem>
)} )}
</Link> {isCmek && (
<Link
to={`/${ProjectType.KMS}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
{isSSH && (
<Link
to={`/${ProjectType.SSH}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/integrations` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="jigsaw-puzzle">
{t("nav.menu.integrations")}
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/secret-rotation` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="rotation">
Secret Rotation
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/approval` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="circular-check">
Approvals
{Boolean(
secretApprovalReqCount?.open ||
accessApprovalRequestCount?.pendingCount
) && (
<span className="ml-2 rounded border border-primary-400 bg-primary-600 px-1 py-0.5 text-xs font-semibold text-black">
{pendingRequestsCount}
</span>
)}
</MenuItem>
)}
</Link>
)}
</MenuGroup>
<MenuGroup title="Other">
<Link
to={`/${currentWorkspace.type}/$projectId/access-management` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="groups">
Access Control
</MenuItem>
)}
</Link>
<Link
to={`/${currentWorkspace.type}/$projectId/settings` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
{t("nav.menu.project-settings")}
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu> </Menu>
</div> </div>
</div> </div>
<SidebarFooter />
</nav> </nav>
</aside> </motion.div>
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]"> <div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]">
{breadcrumbs ? (
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
) : null}
<Outlet /> <Outlet />
</main> </div>
</div> </div>
</div> </div>
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden"> <div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
@@ -167,7 +167,7 @@ export const ProjectSelect = () => {
}, [workspaces, projectFavorites, currentWorkspace]); }, [workspaces, projectFavorites, currentWorkspace]);
return ( return (
<div className="mb-4 mt-5 w-full p-3"> <div className="mt-2 w-full p-3">
<p className="mb-1 ml-1.5 text-xs font-semibold uppercase text-gray-400"> <p className="mb-1 ml-1.5 text-xs font-semibold uppercase text-gray-400">
{currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"} {currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"}
</p> </p>
@@ -80,7 +80,7 @@ export const LoginPage = () => {
/> />
</div> </div>
</Link> </Link>
<div className="pb-28">{renderView()}</div>; <div className="pb-28">{renderView()}</div>
</div> </div>
); );
}; };
@@ -59,7 +59,7 @@ export const LoginSsoPage = () => {
/> />
</div> </div>
</Link> </Link>
<div>{renderView()}</div>; <div>{renderView()}</div>
</div> </div>
); );
}; };
@@ -85,8 +85,7 @@ export const SelectOrganizationPage = () => {
} }
} }
window.open(url); window.location.href = url;
window.close();
return; return;
} }
@@ -89,7 +89,7 @@ export const SignupSsoPage = () => {
alt="Infisical Logo" alt="Infisical Logo"
/> />
</div> </div>
<div>{renderView()}</div>; <div>{renderView()}</div>
</div> </div>
); );
}; };
@@ -1,6 +1,4 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -13,6 +11,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -80,34 +79,17 @@ const Page = () => {
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader title={data.friendlyName}>
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() =>
navigate({
to: `/${ProjectType.CertificateManager}/$projectId/overview` as const,
params: {
projectId
}
})
}
className="mb-4"
>
Certificate Authorities
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.friendlyName}</p>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="end" className="p-1">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.CertificateAuthorities} a={ProjectPermissionSub.CertificateAuthorities}
@@ -133,7 +115,7 @@ const Page = () => {
</ProjectPermissionCan> </ProjectPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<CaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} /> <CaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} />
@@ -1,9 +1,25 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { CertAuthDetailsByIDPage } from "./CertAuthDetailsByIDPage"; import { CertAuthDetailsByIDPage } from "./CertAuthDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId"
)({ )({
component: CertAuthDetailsByIDPage component: CertAuthDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Certificate Authorities",
link: linkOptions({
to: "/cert-manager/$projectId/overview",
params: {
projectId: params.projectId
}
})
}
]
};
}
}); });
@@ -2,7 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { CaTab, CertificatesTab, PkiAlertsTab } from "./components"; import { CaTab, CertificatesTab, PkiAlertsTab } from "./components";
@@ -19,11 +19,9 @@ export const CertificatesPage = () => {
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "Certificates" })}</title> <title>{t("common.head-title", { title: "Certificates" })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<p className="mb-4 mr-4 text-3xl font-semibold text-white">Internal PKI</p> <PageHeader title="Overview" />
<Tabs defaultValue={TabSections.Certificates}> <Tabs defaultValue={TabSections.Certificates}>
<TabList> <TabList>
<Tab value={TabSections.Certificates}>Certificates</Tab> <Tab value={TabSections.Certificates}>Certificates</Tab>
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { CertificatesPage } from "./CertificatesPage"; import { CertificatesPage } from "./CertificatesPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/overview" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview"
)({ )({
component: CertificatesPage component: CertificatesPage
}); });
@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -14,6 +12,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -74,34 +73,17 @@ export const PkiCollectionPage = () => {
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader title={data.name}>
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: `/${ProjectType.CertificateManager}/$projectId/overview` as const,
params: {
projectId
}
});
}}
className="mb-4"
>
Certificate Collections
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.name}</p>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="end" className="p-1">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.PkiCollections} a={ProjectPermissionSub.PkiCollections}
@@ -147,7 +129,7 @@ export const PkiCollectionPage = () => {
</ProjectPermissionCan> </ProjectPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<PkiCollectionDetailsSection <PkiCollectionDetailsSection
@@ -1,9 +1,25 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { PkiCollectionDetailsByIDPage } from "./PkiCollectionDetailsByIDPage"; import { PkiCollectionDetailsByIDPage } from "./PkiCollectionDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
)({ )({
component: PkiCollectionDetailsByIDPage component: PkiCollectionDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Certificate Collections",
link: linkOptions({
to: "/cert-manager/$projectId/overview",
params: {
projectId: params.projectId
}
})
}
]
};
}
}); });
@@ -1,7 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab"; import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
@@ -14,12 +14,9 @@ export const SettingsPage = () => {
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title> <title>{t("common.head-title", { title: t("settings.project.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<div className="w-full max-w-7xl px-6"> <div className="w-full max-w-7xl">
<div className="my-6"> <PageHeader title={t("settings.project.title")} />
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
</div>
<Tabs defaultValue={tabs[0].key}> <Tabs defaultValue={tabs[0].key}>
<TabList> <TabList>
{tabs.map((tab) => ( {tabs.map((tab) => (
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
import { SettingsPage } from "./SettingsPage"; import { SettingsPage } from "./SettingsPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/settings" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings"
)({ )({
component: SettingsPage component: SettingsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Settings"
}
]
};
}
}); });
+19 -3
View File
@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { workspaceKeys } from "@app/hooks/api"; import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
@@ -6,11 +6,11 @@ import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { ProjectLayout } from "@app/layouts/ProjectLayout"; import { ProjectLayout } from "@app/layouts/ProjectLayout";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout" "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
)({ )({
component: ProjectLayout, component: ProjectLayout,
beforeLoad: async ({ params, context }) => { beforeLoad: async ({ params, context }) => {
await context.queryClient.ensureQueryData({ const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId), queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId) queryFn: () => fetchWorkspaceById(params.projectId)
}); });
@@ -21,5 +21,21 @@ export const Route = createFileRoute(
}), }),
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId }) queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
}); });
return {
breadcrumbs: [
{
label: "Cert Managers",
link: linkOptions({ to: "/organization/cert-manager/overview" })
},
{
label: project.name,
link: linkOptions({
to: "/cert-manager/$projectId/overview",
params: { projectId: project.id }
})
}
]
};
} }
}); });
@@ -2,6 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { CmekTable } from "./components"; import { CmekTable } from "./components";
@@ -13,15 +14,13 @@ export const OverviewPage = () => {
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
<Helmet> <Helmet>
<title>{t("common.head-title", { title: "KMS" })}</title> <title>{t("common.head-title", { title: "KMS" })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <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 px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<p className="mr-4 text-3xl font-semibold text-white">Key Management System</p> <PageHeader
<p className="text-md mb-4 text-bunker-300"> title="OverviewPage"
Manage keys and perform cryptographic operations. description="Manage keys and perform cryptographic operations."
</p> />
<ProjectPermissionCan <ProjectPermissionCan
passThrough={false} passThrough={false}
renderGuardBanner renderGuardBanner
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { OverviewPage } from "./OverviewPage"; import { OverviewPage } from "./OverviewPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/overview" "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview"
)({ )({
component: OverviewPage component: OverviewPage
}); });
@@ -1,7 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab"; import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
@@ -14,12 +14,9 @@ export const SettingsPage = () => {
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title> <title>{t("common.head-title", { title: t("settings.project.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<div className="w-full max-w-7xl px-6"> <div className="w-full max-w-7xl">
<div className="my-6"> <PageHeader title={t("settings.project.title")} />
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
</div>
<Tabs defaultValue={tabs[0].key}> <Tabs defaultValue={tabs[0].key}>
<TabList> <TabList>
{tabs.map((tab) => ( {tabs.map((tab) => (
+12 -2
View File
@@ -3,7 +3,17 @@ import { createFileRoute } from "@tanstack/react-router";
import { SettingsPage } from "./SettingsPage"; import { SettingsPage } from "./SettingsPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/settings" "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings"
)({ )({
component: SettingsPage component: SettingsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Settings"
}
]
};
}
}); });
+19 -3
View File
@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { workspaceKeys } from "@app/hooks/api"; import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
@@ -6,11 +6,11 @@ import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { ProjectLayout } from "@app/layouts/ProjectLayout"; import { ProjectLayout } from "@app/layouts/ProjectLayout";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout" "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout"
)({ )({
component: ProjectLayout, component: ProjectLayout,
beforeLoad: async ({ params, context }) => { beforeLoad: async ({ params, context }) => {
await context.queryClient.ensureQueryData({ const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId), queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId) queryFn: () => fetchWorkspaceById(params.projectId)
}); });
@@ -21,5 +21,21 @@ export const Route = createFileRoute(
}), }),
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId }) queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
}); });
return {
breadcrumbs: [
{
label: "KMS",
link: linkOptions({ to: "/organization/kms/overview" })
},
{
label: project.name,
link: linkOptions({
to: "/kms/$projectId/overview",
params: { projectId: project.id }
})
}
]
};
} }
}); });
@@ -2,7 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useSearch } from "@tanstack/react-router"; import { useNavigate, useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgAccessControlTabSections } from "@app/types/org"; import { OrgAccessControlTabSections } from "@app/types/org";
@@ -59,8 +59,11 @@ export const AccessManagementPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
</Helmet> </Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<p className="mb-4 mr-4 text-3xl font-semibold text-white">Organization Access Control</p> <PageHeader
title="Organization Access Control"
description="Manage fine-grained access for users, groups, roles, and identities within your organization resources."
/>
<Tabs value={selectedTab} onValueChange={updateSelectedTab}> <Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
{tabSections {tabSections
@@ -44,13 +44,13 @@ import {
} from "@app/context"; } from "@app/context";
import { usePagination, useResetPageHelper } from "@app/hooks"; import { usePagination, useResetPageHelper } from "@app/hooks";
import { import {
useAddUsersToOrg,
useFetchServerStatus, useFetchServerStatus,
useGetOrgRoles, useGetOrgRoles,
useGetOrgUsers, useGetOrgUsers,
useUpdateOrgMembership useUpdateOrgMembership
} from "@app/hooks/api"; } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useResendOrgMemberInvitation } from "@app/hooks/api/users/mutation";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
@@ -83,7 +83,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
const { data: serverDetails } = useFetchServerStatus(); const { data: serverDetails } = useFetchServerStatus();
const { data: members = [], isPending: isMembersLoading } = useGetOrgUsers(orgId); const { data: members = [], isPending: isMembersLoading } = useGetOrgUsers(orgId);
const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg(); const { mutateAsync: resendOrgMemberInvitation } = useResendOrgMemberInvitation();
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership(); const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
const onRoleChange = async (membershipId: string, role: string) => { const onRoleChange = async (membershipId: string, role: string) => {
@@ -119,26 +119,25 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
} }
}; };
const onResendInvite = async (email: string) => { const onResendInvite = async (membershipId: string) => {
try { try {
const { data } = await addUsersMutateAsync({ const signupToken = await resendOrgMemberInvitation({
organizationId: orgId, membershipId
inviteeEmails: [email],
organizationRoleSlug: "member"
}); });
setCompleteInviteLinks(data?.completeInviteLinks || null); if (signupToken) {
setCompleteInviteLinks([signupToken]);
if (!data.completeInviteLinks) { return;
createNotification({
text: `Successfully resent invite to ${email}`,
type: "success"
});
} }
createNotification({
text: "Successfully resent org invitation",
type: "success"
});
} catch (err) { } catch (err) {
console.error(err); console.error(err);
createNotification({ createNotification({
text: `Failed to resend invite to ${email}`, text: "Failed to resend org invitation",
type: "error" type: "error"
}); });
} }
@@ -370,7 +369,10 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
className="w-48" className="w-48"
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
onClick={() => onResendInvite(email)} onClick={(e) => {
onResendInvite(orgMembershipId);
e.stopPropagation();
}}
> >
Resend invite Resend invite
</Button> </Button>
@@ -1,4 +1,6 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter"; import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod"; import { z } from "zod";
@@ -12,12 +14,24 @@ const AccessControlPageQuerySchema = z.object({
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/access-management" "/_authenticate/_inject-org-details/_org-layout/organization/access-management"
)({ )({
component: AccessManagementPage, component: AccessManagementPage,
validateSearch: zodValidator(AccessControlPageQuerySchema), validateSearch: zodValidator(AccessControlPageQuerySchema),
search: { search: {
// strip default values // strip default values
middlewares: [stripSearchParams({ action: "" })] middlewares: [stripSearchParams({ action: "" })]
} },
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "access control"
}
]
})
}); });
@@ -2,7 +2,7 @@ import { useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgAdminProjects } from "./components/OrgAdminProjects"; import { OrgAdminProjects } from "./components/OrgAdminProjects";
@@ -17,13 +17,13 @@ export const AdminPage = () => {
<> <>
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-6xl px-6"> <div className="w-full max-w-7xl">
<div className="mb-4"> <PageHeader
<p className="text-3xl font-semibold text-gray-200">Organization Admin Console</p> title="Organization Admin Console"
</div> description="View and manage resources across your organization."
/>
<Tabs value={activeTab} onValueChange={(el) => setActiveTab(el as TabSections)}> <Tabs value={activeTab} onValueChange={(el) => setActiveTab(el as TabSections)}>
<TabList> <TabList>
<Tab value={TabSections.Projects}>Projects</Tab> <Tab value={TabSections.Projects}>Projects</Tab>
@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { AdminPage } from "./AdminPage"; import { AdminPage } from "./AdminPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/admin" "/_authenticate/_inject-org-details/_org-layout/organization/admin"
)({ )({
component: AdminPage component: AdminPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Admin Console"
}
]
})
}); });
@@ -11,7 +11,7 @@ const GitHubOAuthCallbackPageQueryParamsSchema = z.object({
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback" "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback"
)({ )({
component: GitHubOAuthCallbackPage, component: GitHubOAuthCallbackPage,
validateSearch: zodValidator(GitHubOAuthCallbackPageQueryParamsSchema), validateSearch: zodValidator(GitHubOAuthCallbackPageQueryParamsSchema),
@@ -1,5 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { PageHeader } from "@app/components/v2";
import { LogsSection } from "./components"; import { LogsSection } from "./components";
export const AuditLogsPage = () => { export const AuditLogsPage = () => {
@@ -11,11 +13,11 @@ export const AuditLogsPage = () => {
<meta property="og:image" content="/images/message.png" /> <meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6"> <div className="w-full max-w-7xl">
<div className="bg-bunker-800 py-6"> <PageHeader
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p> title="Audit logs"
<div /> description="Audit logs for security and compliance teams to monitor information access."
</div> />
<LogsSection filterClassName="static py-2" showFilters isOrgAuditLogs showActorColumn /> <LogsSection filterClassName="static py-2" showFilters isOrgAuditLogs showActorColumn />
</div> </div>
</div> </div>
@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { AuditLogsPage } from "./AuditLogsPage"; import { AuditLogsPage } from "./AuditLogsPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/audit-logs" "/_authenticate/_inject-org-details/_org-layout/organization/audit-logs"
)({ )({
component: AuditLogsPage component: AuditLogsPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Audit Logs"
}
]
})
}); });
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { BillingPage } from "./BillingPage"; import { BillingPage } from "./BillingPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/billing" "/_authenticate/_inject-org-details/_org-layout/organization/billing"
)({ )({
component: BillingPage component: BillingPage
}); });
@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { CertManagerOverviewPage } from "./CertManagerOverviewPage"; import { CertManagerOverviewPage } from "./CertManagerOverviewPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/cert-manager/overview" "/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview"
)({ )({
component: CertManagerOverviewPage component: CertManagerOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Cert Management",
link: linkOptions({ to: "/organization/cert-manager/overview" })
}
]
})
}); });
@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -15,6 +13,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Spinner, Spinner,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
@@ -83,34 +82,17 @@ const Page = () => {
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader title={data.group.name}>
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management" as const,
search: {
selectedTab: TabSections.Groups
}
});
}}
className="mb-4"
>
Groups
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.group.name}</p>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="end" className="p-1">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}> <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -153,7 +135,7 @@ const Page = () => {
</OrgPermissionCan> </OrgPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<GroupDetailsSection groupId={groupId} handlePopUpOpen={handlePopUpOpen} /> <GroupDetailsSection groupId={groupId} handlePopUpOpen={handlePopUpOpen} />
@@ -1,9 +1,27 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage"; import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/groups/$groupId" "/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId"
)({ )({
component: GroupDetailsByIDPage component: GroupDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "groups"
}
]
})
}); });
@@ -1,8 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -16,6 +14,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -165,30 +164,13 @@ const Page = () => {
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader title={data.identity.name}>
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management",
search: {
selectedTab: OrgAccessControlTabSections.Identities
}
});
}}
className="mb-4"
>
Identities
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.identity.name}</p>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -257,7 +239,7 @@ const Page = () => {
</OrgPermissionCan> </OrgPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} /> <IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
@@ -1,9 +1,27 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/identities/$identityId" "/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId"
)({ )({
component: IdentityDetailsByIDPage component: IdentityDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "identities"
}
]
})
}); });
@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { KmsOverviewPage } from "./KmsOverviewPage"; import { KmsOverviewPage } from "./KmsOverviewPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/kms/overview" "/_authenticate/_inject-org-details/_org-layout/organization/kms/overview"
)({ )({
component: KmsOverviewPage component: KmsOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "KMS",
link: linkOptions({ to: "/organization/kms/overview" })
}
]
})
}); });
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { NoOrgPage } from "./NoOrgPage"; import { NoOrgPage } from "./NoOrgPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/none" "/_authenticate/_inject-org-details/_org-layout/organization/none"
)({ )({
component: NoOrgPage component: NoOrgPage
}); });
@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -14,6 +12,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -78,31 +77,14 @@ export const Page = () => {
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader title={data.name}>
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management" as const,
search: {
selectedTab: OrgAccessControlTabSections.Roles
}
});
}}
className="mb-4"
>
Roles
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.name}</p>
{isCustomRole && ( {isCustomRole && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options"> <Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <Button variant="outline_bg">More</Button>
</Tooltip> </Tooltip>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -144,7 +126,7 @@ export const Page = () => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
</div> </PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<RoleDetailsSection roleId={roleId} handlePopUpOpen={handlePopUpOpen} /> <RoleDetailsSection roleId={roleId} handlePopUpOpen={handlePopUpOpen} />
@@ -1,9 +1,27 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { RoleByIDPage } from "./RoleByIDPage"; import { RoleByIDPage } from "./RoleByIDPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/roles/$roleId" "/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId"
)({ )({
component: RoleByIDPage component: RoleByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "Roles"
}
]
})
}); });
@@ -22,7 +22,15 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { NewProjectModal } from "@app/components/projects"; import { NewProjectModal } from "@app/components/projects";
import { Button, IconButton, Input, Pagination, Skeleton, Tooltip } from "@app/components/v2"; import {
Button,
IconButton,
Input,
PageHeader,
Pagination,
Skeleton,
Tooltip
} from "@app/components/v2";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
@@ -50,13 +58,6 @@ enum ProjectOrderBy {
Name = "name" Name = "name"
} }
const formatTitle = (type: ProjectType) => {
if (type === ProjectType.SecretManager) return "Secret Management";
if (type === ProjectType.CertificateManager) return "Cert Management";
if (type === ProjectType.KMS) return "Key Management";
return "SSH";
};
const formatDescription = (type: ProjectType) => { const formatDescription = (type: ProjectType) => {
if (type === ProjectType.SecretManager) if (type === ProjectType.SecretManager)
return "Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc."; return "Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc.";
@@ -365,13 +366,13 @@ export const ProductOverviewPage = ({ type }: Props) => {
} }
return ( return (
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen"> <div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800">
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.members.title") })}</title> <title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
{!serverDetails?.redisConfigured && ( {!serverDetails?.redisConfigured && (
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl"> <div className="mb-4 flex flex-col items-start justify-start text-3xl">
<p className="mb-4 mr-4 font-semibold text-white">Announcements</p> <p className="mb-4 mr-4 font-semibold text-white">Announcements</p>
<div className="flex w-full items-center rounded-md border border-blue-400/70 bg-blue-900/70 p-2 text-base text-mineshaft-100"> <div className="flex w-full items-center rounded-md border border-blue-400/70 bg-blue-900/70 p-2 text-base text-mineshaft-100">
<FontAwesomeIcon <FontAwesomeIcon
@@ -393,14 +394,9 @@ export const ProductOverviewPage = ({ type }: Props) => {
</div> </div>
</div> </div>
)} )}
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0"> <div className="mb-4 flex flex-col items-start justify-start">
<div className="flex w-full justify-between"> <PageHeader title="Projects" description={formatDescription(type)} />
<p className="mr-4 text-3xl font-semibold text-white">{formatTitle(type)}</p> <div className="flex w-full flex-row">
</div>
<div>
<p className="mr-4 mt-2 text-gray-400">{formatDescription(type)}</p>
</div>
<div className="mt-6 flex w-full flex-row">
<Input <Input
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80" className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
placeholder="Search by project name..." placeholder="Search by project name..."
@@ -1,9 +1,22 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { SecretManagerOverviewPage } from "./SecretManagerOverviewPage"; import { SecretManagerOverviewPage } from "./SecretManagerOverviewPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/secret-manager/overview" "/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/overview"
)({ )({
component: SecretManagerOverviewPage component: SecretManagerOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Secret Management"
}
]
})
}); });
@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useSearch } from "@tanstack/react-router"; import { useSearch } from "@tanstack/react-router";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button, NoticeBanner, Pagination } from "@app/components/v2"; import { Button, NoticeBanner, PageHeader, Pagination } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { import {
OrgPermissionActions, OrgPermissionActions,
@@ -111,11 +111,11 @@ export const SecretScanningPage = withPermission(
<meta property="og:image" content="/images/message.png" /> <meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6"> <div className="w-full max-w-7xl">
<div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div> <PageHeader
<div className="mb-6 text-lg text-mineshaft-300"> title="Secret Scanning"
Automatically monitor your GitHub activity and prevent secret leaks description="Automatically monitor your GitHub activity and prevent secret leaks"
</div> />
{config.isSecretScanningDisabled && ( {config.isSecretScanningDisabled && (
<NoticeBanner title="Secret scanning is in maintenance" className="mb-4"> <NoticeBanner title="Secret scanning is in maintenance" className="mb-4">
We are working on improving the performance of secret scanning due to increased We are working on improving the performance of secret scanning due to increased
@@ -1,4 +1,6 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter"; import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod"; import { z } from "zod";
@@ -10,10 +12,10 @@ const SecretScanningQueryParams = z.object({
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/secret-scanning" "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
)({ )({
component: SecretScanningPage,
validateSearch: zodValidator(SecretScanningQueryParams), validateSearch: zodValidator(SecretScanningQueryParams),
component: SecretScanningPage,
search: { search: {
middlewares: [ middlewares: [
stripSearchParams({ stripSearchParams({
@@ -21,5 +23,17 @@ export const Route = createFileRoute(
state: "" state: ""
}) })
] ]
} },
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "Secret Scanning"
}
]
})
}); });
@@ -3,6 +3,8 @@ import { useTranslation } from "react-i18next";
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PageHeader } from "@app/components/v2";
import { ShareSecretSection } from "./components"; import { ShareSecretSection } from "./components";
export const SecretSharingPage = () => { export const SecretSharingPage = () => {
@@ -18,28 +20,25 @@ export const SecretSharingPage = () => {
<meta name="og:description" content={String(t("approval.og-description"))} /> <meta name="og:description" content={String(t("approval.og-description"))} />
</Helmet> </Helmet>
<div className="h-full"> <div className="h-full">
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 px-6 text-white"> <div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 text-white">
<div className="flex items-center justify-between py-6"> <PageHeader
<div className="flex w-full flex-col"> title="Secret Sharing"
<h2 className="text-3xl font-semibold text-gray-200">Secret Sharing</h2> description="Share secrets securely using a shareable link"
<p className="text-bunker-300">Share secrets securely using a shareable link</p> >
</div> <a
<div className="flex w-max justify-center"> target="_blank"
<a rel="noopener noreferrer"
target="_blank" href="https://infisical.com/docs/documentation/platform/secret-sharing"
rel="noopener noreferrer" >
href="https://infisical.com/docs/documentation/platform/secret-sharing" <div 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{" "}
<div 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"> <FontAwesomeIcon
Documentation{" "} icon={faArrowUpRightFromSquare}
<FontAwesomeIcon className="mb-[0.06rem] ml-1 text-xs"
icon={faArrowUpRightFromSquare} />
className="mb-[0.06rem] ml-1 text-xs" </div>
/> </a>
</div> </PageHeader>
</a>
</div>
</div>
<ShareSecretSection /> <ShareSecretSection />
</div> </div>
</div> </div>
@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SecretSharingPage } from "./SecretSharingPage"; import { SecretSharingPage } from "./SecretSharingPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/secret-sharing" "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
)({ )({
component: SecretSharingPage component: SecretSharingPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "secret sharing"
}
]
})
}); });
@@ -1,6 +1,8 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { OrgTabGroup } from "./components"; import { OrgTabGroup } from "./components";
export const SettingsPage = () => { export const SettingsPage = () => {
@@ -11,11 +13,9 @@ export const SettingsPage = () => {
<Helmet> <Helmet>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
</Helmet> </Helmet>
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6"> <div className="w-full max-w-7xl">
<div className="mb-4"> <PageHeader title={t("settings.org.title")} />
<p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
</div>
<OrgTabGroup /> <OrgTabGroup />
</div> </div>
</div> </div>
@@ -103,7 +103,7 @@ export const AuditLogStreamForm = ({ id = "", onClose }: Props) => {
console.log(err); console.log(err);
createNotification({ createNotification({
type: "error", type: "error",
text: "Failed to create stream" text: (err as Error)?.message ?? "Failed to create stream"
}); });
} }
}; };
@@ -103,7 +103,13 @@ export const OrgNameChangeSection = (): JSX.Element => {
name="name" name="name"
/> />
</div> </div>
<div className="py-4"> <div>
<h2 className="text-md mb-2 text-mineshaft-100">Organization ID</h2>
<FormControl className="max-w-md">
<Input isDisabled value={currentOrg.id} />
</FormControl>
</div>
<div>
<h2 className="text-md mb-2 text-mineshaft-100">Organization Slug</h2> <h2 className="text-md mb-2 text-mineshaft-100">Organization Slug</h2>
<Controller <Controller
defaultValue="" defaultValue=""
@@ -1,4 +1,6 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter"; import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod"; import { z } from "zod";
@@ -9,11 +11,23 @@ const SettingsPageQueryParams = z.object({
}); });
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/settings" "/_authenticate/_inject-org-details/_org-layout/organization/settings"
)({ )({
component: SettingsPage, component: SettingsPage,
validateSearch: zodValidator(SettingsPageQueryParams), validateSearch: zodValidator(SettingsPageQueryParams),
search: { search: {
middlewares: [stripSearchParams({ selectedTab: "" })] middlewares: [stripSearchParams({ selectedTab: "" })]
} },
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/" })
},
{
label: "Settings"
}
]
})
}); });
@@ -1,9 +1,23 @@
import { createFileRoute } from "@tanstack/react-router"; import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SshOverviewPage } from "./SshOverviewPage"; import { SshOverviewPage } from "./SshOverviewPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/organization/_layout/ssh/overview" "/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview"
)({ )({
component: SshOverviewPage component: SshOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "SSH",
link: linkOptions({ to: "/organization/ssh/overview" })
}
]
})
}); });
@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -15,6 +13,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
@@ -118,143 +117,132 @@ const Page = withPermission(
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{membership && ( {membership && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl">
<Button <PageHeader
variant="link" title={
type="submit" membership.user.firstName || membership.user.lastName
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
navigate({
to: "/organization/access-management" as const,
search: {
selectedTab: OrgAccessControlTabSections.Member
}
});
}}
className="mb-4"
>
Users
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">
{membership.user.firstName || membership.user.lastName
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim() ? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
: "-"} : "-"
</p> }
{userId !== membership.user.id && ( >
<DropdownMenu> <div>
<DropdownMenuTrigger asChild className="rounded-lg"> {userId !== membership.user.id && (
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <DropdownMenu>
<Tooltip content="More options"> <DropdownMenuTrigger asChild className="rounded-lg">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
</Tooltip> <Tooltip content="More options">
</div> <Button variant="outline_bg" size="sm">
</DropdownMenuTrigger> More
<DropdownMenuContent align="start" className="p-1"> </Button>
<OrgPermissionCan </Tooltip>
I={OrgPermissionActions.Edit} </div>
a={OrgPermissionSubjects.Identity} </DropdownMenuTrigger>
> <DropdownMenuContent align="end" className="p-1">
{(isAllowed) => ( <OrgPermissionCan
<DropdownMenuItem I={OrgPermissionActions.Edit}
className={twMerge( a={OrgPermissionSubjects.Identity}
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50" >
)} {(isAllowed) => (
onClick={() => <DropdownMenuItem
handlePopUpOpen("orgMembership", { className={twMerge(
membershipId: membership.id, !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
role: membership.role, )}
roleId: membership.roleId onClick={() =>
}) handlePopUpOpen("orgMembership", {
} membershipId: membership.id,
disabled={!isAllowed} role: membership.role,
> roleId: membership.roleId
Edit User })
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={
membership.isActive
? twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)
: ""
}
onClick={async () => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
} }
disabled={!isAllowed}
if (!membership.isActive) { >
// activate user Edit User
await updateOrgMembership({ </DropdownMenuItem>
organizationId: orgId, )}
membershipId, </OrgPermissionCan>
isActive: true <OrgPermissionCan
}); I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
return; >
{(isAllowed) => (
<DropdownMenuItem
className={
membership.isActive
? twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)
: ""
} }
onClick={async () => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
}
// deactivate user if (!membership.isActive) {
handlePopUpOpen("deactivateMember", { // activate user
orgMembershipId: membershipId, await updateOrgMembership({
username: membership.user.username organizationId: orgId,
}); membershipId,
}} isActive: true
disabled={!isAllowed} });
>
{`${membership.isActive ? "Deactivate" : "Activate"} User`} return;
</DropdownMenuItem> }
)}
</OrgPermissionCan> // deactivate user
<OrgPermissionCan handlePopUpOpen("deactivateMember", {
I={OrgPermissionActions.Delete} orgMembershipId: membershipId,
a={OrgPermissionSubjects.Member} username: membership.user.username
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
}); });
return; }}
} disabled={!isAllowed}
>
{`${membership.isActive ? "Deactivate" : "Activate"} User`}
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {
if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
}
handlePopUpOpen("removeMember", { handlePopUpOpen("removeMember", {
orgMembershipId: membershipId, orgMembershipId: membershipId,
username: membership.user.username username: membership.user.username
}); });
}} }}
disabled={!isAllowed} disabled={!isAllowed}
> >
Remove User Remove User
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
</div> </div>
</PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} /> <UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />

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