mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 22:28:25 +00:00
chore: resolve merge conflicts
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
|
||||||
|
if (existingGroup) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to create group with name '${name}'. Group with the same name already exists`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newGroup = await groupDAL.create(
|
||||||
|
{
|
||||||
name,
|
name,
|
||||||
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`),
|
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`),
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
role: isCustomRole ? OrgMembershipRole.Custom : role,
|
role: isCustomRole ? OrgMembershipRole.Custom : role,
|
||||||
roleId: customRole?.id
|
roleId: customRole?.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return newGroup;
|
||||||
});
|
});
|
||||||
|
|
||||||
return group;
|
return group;
|
||||||
@@ -145,7 +159,18 @@ 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) {
|
||||||
|
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
|
||||||
|
|
||||||
|
if (existingGroup && existingGroup.id !== id) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to update group with name '${name}'. Group with the same name already exists`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updated] = await groupDAL.update(
|
||||||
{
|
{
|
||||||
id: group.id
|
id: group.id
|
||||||
},
|
},
|
||||||
@@ -158,9 +183,13 @@ export const groupServiceFactory = ({
|
|||||||
roleId: customRole?.id ?? null
|
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."
|
||||||
|
|||||||
@@ -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(",")}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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;
|
||||||
@@ -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,6 +932,8 @@ 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}]`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
await ssm
|
await ssm
|
||||||
.putParameter({
|
.putParameter({
|
||||||
Name: `${integration.path}${key}`,
|
Name: `${integration.path}${key}`,
|
||||||
@@ -941,6 +943,11 @@ const syncSecretsAWSParameterStore = async ({
|
|||||||
Overwrite: true
|
Overwrite: true
|
||||||
})
|
})
|
||||||
.promise();
|
.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,6 +994,7 @@ 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)) {
|
||||||
|
try {
|
||||||
await ssm
|
await ssm
|
||||||
.putParameter({
|
.putParameter({
|
||||||
Name: `${integration.path}${key}`,
|
Name: `${integration.path}${key}`,
|
||||||
@@ -996,6 +1004,10 @@ const syncSecretsAWSParameterStore = async ({
|
|||||||
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId })
|
...(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) {
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,14 +447,33 @@ 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) {
|
||||||
|
const projectsLinkedToForiegnKey = await projectDAL.find(
|
||||||
|
{ kmsCertificateKeyId: delProject.kmsCertificateKeyId },
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
if (!projectsLinkedToForiegnKey.length) {
|
||||||
await kmsService.deleteInternalKms(delProject.kmsCertificateKeyId, delProject.orgId, tx);
|
await kmsService.deleteInternalKms(delProject.kmsCertificateKeyId, delProject.orgId, tx);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (delProject.kmsSecretManagerKeyId) {
|
if (delProject.kmsSecretManagerKeyId) {
|
||||||
|
const projectsLinkedToForiegnKey = await projectDAL.find(
|
||||||
|
{ kmsSecretManagerKeyId: delProject.kmsSecretManagerKeyId },
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
if (!projectsLinkedToForiegnKey.length) {
|
||||||
await kmsService.deleteInternalKms(delProject.kmsSecretManagerKeyId, delProject.orgId, tx);
|
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) {
|
||||||
await userDAL.deleteById(projectGhostUser.id, tx);
|
await userDAL.deleteById(projectGhostUser.id, tx);
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -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,21 +37,18 @@ 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"
|
||||||
|
role="menuitem"
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"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",
|
"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",
|
||||||
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
|
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
|
||||||
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
<motion.span className="flex w-full flex-row items-center justify-start rounded-sm">
|
|
||||||
<Item
|
|
||||||
type="button"
|
|
||||||
role="menuitem"
|
|
||||||
className="relative flex items-center"
|
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
onMouseEnter={() => iconRef.current?.play()}
|
||||||
|
onMouseLeave={() => iconRef.current?.stop()}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -73,11 +69,8 @@ export const MenuItem = <T extends ElementType = "button">({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span className="flex-grow text-left">{children}</span>
|
<span className="flex-grow text-left">{children}</span>
|
||||||
</Item>
|
|
||||||
{description && <span className="mt-2 text-xs">{description}</span>}
|
{description && <span className="mt-2 text-xs">{description}</span>}
|
||||||
</motion.span>
|
</Item>
|
||||||
</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,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";
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,131 +1,76 @@
|
|||||||
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 }}
|
||||||
|
exit={{ x: -150 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="dark w-60 overflow-hidden border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900"
|
||||||
|
>
|
||||||
|
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
|
||||||
|
<div className="p-2 pt-3">
|
||||||
|
<SidebarHeader />
|
||||||
|
</div>
|
||||||
|
<Menu>
|
||||||
|
<MenuGroup title="Organization Control">
|
||||||
|
<Link to="/organization/audit-logs">
|
||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<MenuItem isSelected={isActive} icon="sliding-carousel">
|
<MenuItem isSelected={isActive} icon="moving-block">
|
||||||
Secret Management
|
Audit Logs
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
<Link to={`/organization/${ProjectType.CertificateManager}/overview` as const}>
|
|
||||||
{({ isActive }) => (
|
|
||||||
<MenuItem isSelected={isActive} icon="note">
|
|
||||||
Cert Management
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</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>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -140,10 +85,12 @@ export const OrganizationLayout = () => {
|
|||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<Link to="/organization/audit-logs">
|
</MenuGroup>
|
||||||
|
<MenuGroup title="Other">
|
||||||
|
<Link to="/organization/access-management">
|
||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<MenuItem isSelected={isActive} icon="moving-block">
|
<MenuItem isSelected={isActive} icon="groups">
|
||||||
Audit Logs
|
Access Control
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -154,21 +101,29 @@ export const OrganizationLayout = () => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
|
</MenuGroup>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SidebarFooter />
|
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</motion.div>
|
||||||
<CreateOrgModal
|
)}
|
||||||
isOpen={popUp?.createOrg?.isOpen}
|
</AnimatePresence>
|
||||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
<main
|
||||||
/>
|
className={twMerge(
|
||||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
|
"flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]",
|
||||||
|
!isOrganizationSpecificPage && "overflow-hidden p-0"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{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";
|
||||||
+488
@@ -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";
|
|
||||||
+13
-155
@@ -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">
|
|
||||||
<div className="mr-auto flex items-center rounded-md py-1.5 pl-1.5 pr-2 hover:bg-mineshaft-600">
|
|
||||||
<div className="flex h-5 w-5 min-w-[20px] items-center justify-center rounded-md bg-primary text-sm">
|
|
||||||
{currentOrg?.name.charAt(0)}
|
{currentOrg?.name.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="flex flex-grow flex-col text-white">
|
||||||
className="overflow-hidden truncate text-ellipsis pl-2 text-sm text-mineshaft-100"
|
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
|
||||||
style={{ maxWidth: "140px" }}
|
|
||||||
>
|
|
||||||
{currentOrg?.name}
|
{currentOrg?.name}
|
||||||
</div>
|
</div>
|
||||||
<FontAwesomeIcon icon={faAngleDown} className="pl-1 pt-1 text-xs text-mineshaft-300" />
|
<div className="text-xs text-mineshaft-400">{getPlan(subscription)}</div>
|
||||||
</div>
|
</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,52 +45,24 @@ 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>
|
||||||
|
<MenuGroup title="Main Menu">
|
||||||
{isSecretManager && (
|
{isSecretManager && (
|
||||||
<Link
|
<Link
|
||||||
to={`/${ProjectType.SecretManager}/$projectId/overview` as const}
|
to={`/${ProjectType.SecretManager}/$projectId/overview` as const}
|
||||||
@@ -165,18 +119,6 @@ export const ProjectLayout = () => {
|
|||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<Link
|
|
||||||
to={`/${currentWorkspace.type}/$projectId/access-management` as const}
|
|
||||||
params={{
|
|
||||||
projectId: currentWorkspace.id
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ isActive }) => (
|
|
||||||
<MenuItem isSelected={isActive} icon="groups">
|
|
||||||
Access Control
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
{isSecretManager && (
|
{isSecretManager && (
|
||||||
<Link
|
<Link
|
||||||
to={`/${ProjectType.SecretManager}/$projectId/integrations` as const}
|
to={`/${ProjectType.SecretManager}/$projectId/integrations` as const}
|
||||||
@@ -227,6 +169,20 @@ export const ProjectLayout = () => {
|
|||||||
)}
|
)}
|
||||||
</Link>
|
</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
|
<Link
|
||||||
to={`/${currentWorkspace.type}/$projectId/settings` as const}
|
to={`/${currentWorkspace.type}/$projectId/settings` as const}
|
||||||
params={{
|
params={{
|
||||||
@@ -239,15 +195,18 @@ export const ProjectLayout = () => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+6
-24
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
+6
-24
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+15
-13
@@ -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]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!data.completeInviteLinks) {
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: `Successfully resent invite to ${email}`,
|
text: "Successfully resent org invitation",
|
||||||
type: "success"
|
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-23
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
+14
-18
@@ -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,13 +20,11 @@ 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>
|
|
||||||
<div className="flex w-max justify-center">
|
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -38,8 +38,7 @@ export const SecretSharingPage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</PageHeader>
|
||||||
</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>
|
||||||
|
|||||||
+1
-1
@@ -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"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+7
-1
@@ -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,39 +117,27 @@ 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>
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
{userId !== membership.user.id && (
|
{userId !== membership.user.id && (
|
||||||
<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" size="sm">
|
||||||
|
More
|
||||||
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
<DropdownMenuContent align="end" className="p-1">
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Edit}
|
||||||
a={OrgPermissionSubjects.Identity}
|
a={OrgPermissionSubjects.Identity}
|
||||||
@@ -255,6 +242,7 @@ const Page = withPermission(
|
|||||||
</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
Reference in New Issue
Block a user