mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-11 12:11:38 +00:00
Compare commits
2 Commits
create-pol
...
project-ov
Author | SHA1 | Date | |
---|---|---|---|
b467619341 | |||
0f20758df2 |
@ -5,6 +5,7 @@ import crypto, { KeyObject } from "crypto";
|
||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { isValidIp } from "@app/lib/ip";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
@ -795,6 +796,26 @@ export const kmipServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getProjectClientCount = async (projectId: string, actor: OrgServiceActor) => {
|
||||
// Anyone in the project should be able to get count.
|
||||
await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
const clients = await kmipClientDAL.find(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
{ count: true }
|
||||
);
|
||||
|
||||
return Number(clients?.[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
return {
|
||||
createKmipClient,
|
||||
updateKmipClient,
|
||||
@ -806,6 +827,7 @@ export const kmipServiceFactory = ({
|
||||
generateOrgKmipServerCertificate,
|
||||
getOrgKmip,
|
||||
getServerCertificateBySerialNumber,
|
||||
registerServer
|
||||
registerServer,
|
||||
getProjectClientCount
|
||||
};
|
||||
};
|
||||
|
@ -437,6 +437,7 @@ export const secretScanningV2DALFactory = (db: TDbClient) => {
|
||||
return {
|
||||
dataSources: {
|
||||
...dataSourceOrm,
|
||||
findRaw: dataSourceOrm.find,
|
||||
find: findDataSource,
|
||||
findById: findDataSourceById,
|
||||
findOne: findOneDataSource,
|
||||
|
@ -881,6 +881,47 @@ export const secretScanningV2ServiceFactory = ({
|
||||
return config;
|
||||
};
|
||||
|
||||
const getProjectResourcesCount = async (projectId: string, actor: OrgServiceActor) => {
|
||||
// Anyone in the project should be able to get count.
|
||||
await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
const dataSources = await secretScanningV2DAL.dataSources.findRaw(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
{ count: true }
|
||||
);
|
||||
|
||||
const resources = await secretScanningV2DAL.resources.find(
|
||||
{
|
||||
$in: {
|
||||
dataSourceId: dataSources.map((dataSource) => dataSource.id)
|
||||
}
|
||||
},
|
||||
{ count: true }
|
||||
);
|
||||
|
||||
const findings = await secretScanningV2DAL.findings.find(
|
||||
{
|
||||
projectId,
|
||||
status: SecretScanningFindingStatus.Unresolved
|
||||
},
|
||||
{ count: true }
|
||||
);
|
||||
|
||||
return {
|
||||
dataSourceCount: Number(dataSources?.[0]?.count ?? 0),
|
||||
resourceCount: Number(resources?.[0]?.count ?? 0),
|
||||
findingCount: Number(findings?.[0]?.count ?? 0)
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
listSecretScanningDataSourceOptions,
|
||||
listSecretScanningDataSourcesByProjectId,
|
||||
@ -900,6 +941,7 @@ export const secretScanningV2ServiceFactory = ({
|
||||
updateSecretScanningFindingById,
|
||||
findSecretScanningConfigByProjectId,
|
||||
upsertSecretScanningConfig,
|
||||
getProjectResourcesCount,
|
||||
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue),
|
||||
bitbucket: bitbucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService)
|
||||
};
|
||||
|
@ -8,6 +8,7 @@ import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login
|
||||
import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal";
|
||||
import { TSshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-membership-dal";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
@ -414,6 +415,26 @@ export const sshHostGroupServiceFactory = ({
|
||||
return { sshHostGroup, sshHost };
|
||||
};
|
||||
|
||||
const getProjectHostGroupCount = async (projectId: string, actor: OrgServiceActor) => {
|
||||
// Anyone in the project should be able to get count.
|
||||
await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
const hostGroups = await sshHostGroupDAL.find(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
{ count: true }
|
||||
);
|
||||
|
||||
return Number(hostGroups?.[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
return {
|
||||
createSshHostGroup,
|
||||
getSshHostGroup,
|
||||
@ -421,6 +442,7 @@ export const sshHostGroupServiceFactory = ({
|
||||
updateSshHostGroup,
|
||||
listSshHostGroupHosts,
|
||||
addHostToSshHostGroup,
|
||||
removeHostFromSshHostGroup
|
||||
removeHostFromSshHostGroup,
|
||||
getProjectHostGroupCount
|
||||
};
|
||||
};
|
||||
|
@ -13,6 +13,7 @@ import { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ss
|
||||
import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||
import { PgSqlLock } from "@app/keystore/keystore";
|
||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
@ -60,6 +61,7 @@ type TSshHostServiceFactoryDep = {
|
||||
| "findOne"
|
||||
| "findSshHostByIdWithLoginMappings"
|
||||
| "findUserAccessibleSshHosts"
|
||||
| "find"
|
||||
>;
|
||||
sshHostLoginUserDAL: TSshHostLoginUserDALFactory;
|
||||
sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory;
|
||||
@ -637,6 +639,26 @@ export const sshHostServiceFactory = ({
|
||||
return publicKey;
|
||||
};
|
||||
|
||||
const getProjectHostCount = async (projectId: string, actor: OrgServiceActor) => {
|
||||
// Anyone in the project should be able to get count.
|
||||
await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
const hosts = await sshHostDAL.find(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
{ count: true }
|
||||
);
|
||||
|
||||
return Number(hosts?.[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
return {
|
||||
listSshHosts,
|
||||
createSshHost,
|
||||
@ -646,6 +668,7 @@ export const sshHostServiceFactory = ({
|
||||
issueSshHostUserCert,
|
||||
issueSshHostHostCert,
|
||||
getSshHostUserCaPk,
|
||||
getSshHostHostCaPk
|
||||
getSshHostHostCaPk,
|
||||
getProjectHostCount
|
||||
};
|
||||
};
|
||||
|
@ -12,6 +12,11 @@ type TKnexDynamicPrimitiveOperator<T extends object> =
|
||||
operator: "notIn";
|
||||
value: string[];
|
||||
field: Extract<keyof T, string>;
|
||||
}
|
||||
| {
|
||||
operator: "lte";
|
||||
value: Date;
|
||||
field: Extract<keyof T, string>;
|
||||
};
|
||||
|
||||
type TKnexDynamicInOperator<T extends object> = {
|
||||
@ -82,6 +87,10 @@ export const buildDynamicKnexQuery = <T extends object>(
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "lte": {
|
||||
void queryBuilder.where(filterAst.field, "<=", filterAst.value);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new UnauthorizedError({ message: `Invalid knex dynamic operator: ${filterAst.operator}` });
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import { DASHBOARD } from "@app/lib/api-docs";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { secretsLimit } from "@app/server/config/rateLimiter";
|
||||
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -1354,4 +1354,128 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
return { secrets };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/project-overview",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
projectId: z.string(),
|
||||
projectSlug: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
accessControl: z.object({
|
||||
userCount: z.number(),
|
||||
machineIdentityCount: z.number(),
|
||||
groupCount: z.number()
|
||||
}),
|
||||
secretsManagement: z.object({
|
||||
secretCount: z.number(),
|
||||
environmentCount: z.number(),
|
||||
pendingApprovalCount: z.number()
|
||||
}),
|
||||
certificateManagement: z.object({
|
||||
internalCaCount: z.number(),
|
||||
externalCaCount: z.number(),
|
||||
expiryCount: z.number()
|
||||
}),
|
||||
kms: z.object({
|
||||
keyCount: z.number(),
|
||||
kmipClientCount: z.number()
|
||||
}),
|
||||
ssh: z.object({
|
||||
hostCount: z.number(),
|
||||
hostGroupCount: z.number()
|
||||
}),
|
||||
secretScanning: z.object({
|
||||
dataSourceCount: z.number(),
|
||||
resourceCount: z.number(),
|
||||
findingCount: z.number()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId, projectSlug },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const userCount = await server.services.projectMembership.getProjectMembershipCount(projectId, permission);
|
||||
|
||||
const machineIdentityCount = await server.services.identityProject.getProjectIdentityCount(projectId, permission);
|
||||
|
||||
const groupCount = await server.services.groupProject.getProjectGroupCount(projectId, permission);
|
||||
|
||||
const secretsManagement = await server.services.secret.getProjectSecretResourcesCount(projectId, permission);
|
||||
|
||||
const accessApprovals = await server.services.accessApprovalRequest.getCount({
|
||||
projectSlug,
|
||||
actor: permission.type,
|
||||
actorId: permission.id,
|
||||
actorOrgId: permission.orgId,
|
||||
actorAuthMethod: permission.authMethod
|
||||
});
|
||||
|
||||
const secretApprovals = await server.services.secretApprovalRequest.requestCount({
|
||||
projectId,
|
||||
actor: permission.type,
|
||||
actorId: permission.id,
|
||||
actorOrgId: permission.orgId,
|
||||
actorAuthMethod: permission.authMethod
|
||||
});
|
||||
|
||||
const certificateAuthorityCount = await server.services.certificateAuthority.getProjectCertificateAuthorityCount(
|
||||
projectId,
|
||||
permission
|
||||
);
|
||||
|
||||
const expiryCount = await server.services.certificate.getProjectExpiringCertificates(projectId, permission);
|
||||
|
||||
const keyCount = await server.services.cmek.getProjectKeyCount(projectId, permission);
|
||||
|
||||
const kmipClientCount = await server.services.kmip.getProjectClientCount(projectId, permission);
|
||||
|
||||
const hostCount = await server.services.sshHost.getProjectHostCount(projectId, permission);
|
||||
|
||||
const hostGroupCount = await server.services.sshHostGroup.getProjectHostGroupCount(projectId, permission);
|
||||
|
||||
const secretScanning = await server.services.secretScanningV2.getProjectResourcesCount(projectId, permission);
|
||||
|
||||
return {
|
||||
accessControl: {
|
||||
userCount,
|
||||
machineIdentityCount,
|
||||
groupCount
|
||||
},
|
||||
secretsManagement: {
|
||||
...secretsManagement,
|
||||
pendingApprovalCount: accessApprovals.count.pendingCount + secretApprovals.open
|
||||
},
|
||||
certificateManagement: {
|
||||
...certificateAuthorityCount,
|
||||
expiryCount
|
||||
},
|
||||
kms: {
|
||||
keyCount,
|
||||
kmipClientCount
|
||||
},
|
||||
ssh: {
|
||||
hostCount,
|
||||
hostGroupCount
|
||||
},
|
||||
secretScanning
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -47,8 +47,9 @@ type TCertificateAuthorityServiceFactoryDep = {
|
||||
| "findByIdWithAssociatedCa"
|
||||
| "findWithAssociatedCa"
|
||||
| "findByNameAndProjectIdWithAssociatedCa"
|
||||
| "find"
|
||||
>;
|
||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update" | "find">;
|
||||
internalCertificateAuthorityService: TInternalCertificateAuthorityServiceFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
@ -382,11 +383,36 @@ export const certificateAuthorityServiceFactory = ({
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
const getProjectCertificateAuthorityCount = async (projectId: string, actor: OrgServiceActor) => {
|
||||
// Anyone in the project should be able to get count.
|
||||
await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
const cas = await certificateAuthorityDAL.find({ projectId });
|
||||
|
||||
const externalCas = await externalCertificateAuthorityDAL.find(
|
||||
{ $in: { caId: cas.map((ca) => ca.id) } },
|
||||
{ count: true }
|
||||
);
|
||||
|
||||
const externalCaCount = Number(externalCas?.[0]?.count ?? 0);
|
||||
|
||||
return {
|
||||
externalCaCount,
|
||||
internalCaCount: cas.length - externalCaCount
|
||||
};
|
||||
};
|
||||
return {
|
||||
createCertificateAuthority,
|
||||
findCertificateAuthorityByNameAndProjectId,
|
||||
listCertificateAuthoritiesByProjectId,
|
||||
updateCertificateAuthority,
|
||||
deleteCertificateAuthority
|
||||
deleteCertificateAuthority,
|
||||
getProjectCertificateAuthorityCount
|
||||
};
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
@ -600,6 +601,38 @@ export const certificateServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getProjectExpiringCertificates = async (projectId: string, actor: OrgServiceActor) => {
|
||||
// Anyone in the project should be able to get count.
|
||||
await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
const fourteenDaysFromNow = new Date(new Date().setDate(new Date().getDate() + 14));
|
||||
|
||||
const expiringCertificates = await certificateDAL.find(
|
||||
{
|
||||
projectId,
|
||||
$complex: {
|
||||
operator: "and",
|
||||
value: [
|
||||
{
|
||||
operator: "lte",
|
||||
field: "notAfter",
|
||||
value: fourteenDaysFromNow
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{ count: true }
|
||||
);
|
||||
|
||||
return Number(expiringCertificates?.[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
return {
|
||||
getCert,
|
||||
getCertPrivateKey,
|
||||
@ -607,6 +640,7 @@ export const certificateServiceFactory = ({
|
||||
revokeCert,
|
||||
getCertBody,
|
||||
importCert,
|
||||
getCertBundle
|
||||
getCertBundle,
|
||||
getProjectExpiringCertificates
|
||||
};
|
||||
};
|
||||
|
@ -375,6 +375,26 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
||||
};
|
||||
};
|
||||
|
||||
const getProjectKeyCount = async (projectId: string, actor: OrgServiceActor) => {
|
||||
// Anyone in the project should be able to get count.
|
||||
await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
const keys = await kmsDAL.find(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
{ count: true }
|
||||
);
|
||||
|
||||
return Number(keys?.[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
return {
|
||||
createCmek,
|
||||
updateCmekById,
|
||||
@ -387,6 +407,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
||||
cmekSign,
|
||||
cmekVerify,
|
||||
listSigningAlgorithms,
|
||||
getPublicKey
|
||||
getPublicKey,
|
||||
getProjectKeyCount
|
||||
};
|
||||
};
|
||||
|
@ -13,6 +13,7 @@ import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { isUuidV4 } from "@app/lib/validator";
|
||||
|
||||
import { TGroupDALFactory } from "../../ee/services/group/group-dal";
|
||||
@ -33,7 +34,10 @@ import {
|
||||
} from "./group-project-types";
|
||||
|
||||
type TGroupProjectServiceFactoryDep = {
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "findOne" | "transaction" | "create" | "delete" | "findByProjectId">;
|
||||
groupProjectDAL: Pick<
|
||||
TGroupProjectDALFactory,
|
||||
"findOne" | "transaction" | "create" | "delete" | "findByProjectId" | "find"
|
||||
>;
|
||||
groupProjectMembershipRoleDAL: Pick<
|
||||
TGroupProjectMembershipRoleDALFactory,
|
||||
"create" | "transaction" | "insertMany" | "delete"
|
||||
@ -508,12 +512,33 @@ export const groupProjectServiceFactory = ({
|
||||
return { users: members, totalCount };
|
||||
};
|
||||
|
||||
const getProjectGroupCount = async (projectId: string, actor: OrgServiceActor) => {
|
||||
// Anyone in the project should be able to get count.
|
||||
await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
const projectGroups = await groupProjectDAL.find(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
{ count: true }
|
||||
);
|
||||
|
||||
return Number(projectGroups?.[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
return {
|
||||
addGroupToProject,
|
||||
updateGroupInProject,
|
||||
removeGroupFromProject,
|
||||
listGroupsInProject,
|
||||
getGroupInProject,
|
||||
listProjectGroupUsers
|
||||
listProjectGroupUsers,
|
||||
getProjectGroupCount
|
||||
};
|
||||
};
|
||||
|
@ -10,6 +10,7 @@ import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
@ -403,12 +404,33 @@ export const identityProjectServiceFactory = ({
|
||||
return identityMembership;
|
||||
};
|
||||
|
||||
const getProjectIdentityCount = async (projectId: string, actor: OrgServiceActor) => {
|
||||
// Anyone in the project should be able to get count.
|
||||
await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
const identityMemberships = await identityProjectDAL.find(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
{ count: true }
|
||||
);
|
||||
|
||||
return Number(identityMemberships?.[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
return {
|
||||
createProjectIdentity,
|
||||
updateProjectIdentity,
|
||||
deleteProjectIdentity,
|
||||
listProjectIdentities,
|
||||
getProjectIdentityByIdentityId,
|
||||
getProjectIdentityByMembershipId
|
||||
getProjectIdentityByMembershipId,
|
||||
getProjectIdentityCount
|
||||
};
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
@ -567,6 +568,35 @@ export const projectMembershipServiceFactory = ({
|
||||
return deletedMembership;
|
||||
};
|
||||
|
||||
const getProjectMembershipCount = async (projectId: string, actor: OrgServiceActor) => {
|
||||
// Anyone in the project should be able to get count.
|
||||
await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
const projectMemberships = await projectMembershipDAL.find({
|
||||
projectId
|
||||
});
|
||||
|
||||
const users = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
id: projectMemberships.map((membership) => membership.userId)
|
||||
},
|
||||
isGhost: false
|
||||
},
|
||||
{
|
||||
count: true
|
||||
}
|
||||
);
|
||||
|
||||
return Number(users?.[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
return {
|
||||
getProjectMemberships,
|
||||
getProjectMembershipByUsername,
|
||||
@ -575,6 +605,7 @@ export const projectMembershipServiceFactory = ({
|
||||
deleteProjectMembership, // TODO: Remove this
|
||||
addUsersToProject,
|
||||
leaveProject,
|
||||
getProjectMembershipById
|
||||
getProjectMembershipById,
|
||||
getProjectMembershipCount
|
||||
};
|
||||
};
|
||||
|
@ -811,6 +811,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
|
||||
return {
|
||||
...secretOrm,
|
||||
rawFind: secretOrm.find,
|
||||
update,
|
||||
bulkUpdate,
|
||||
deleteMany,
|
||||
|
@ -26,6 +26,7 @@ import { diff, groupBy } from "@app/lib/fn";
|
||||
import { setKnexStringValue } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
@ -86,7 +87,7 @@ type TSecretV2BridgeServiceFactoryDep = {
|
||||
secretTagDAL: TSecretTagDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs" | "find">;
|
||||
folderDAL: Pick<
|
||||
TSecretFolderDALFactory,
|
||||
| "findBySecretPath"
|
||||
@ -2914,6 +2915,39 @@ export const secretV2BridgeServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const getProjectSecretResourcesCount = async (projectId: string, actor: OrgServiceActor) => {
|
||||
// Anyone in the project should be able to get count.
|
||||
await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
const environments = await projectEnvDAL.find({
|
||||
projectId
|
||||
});
|
||||
|
||||
const folders = await folderDAL.find({
|
||||
isReserved: false,
|
||||
$in: {
|
||||
envId: environments.map((env) => env.id)
|
||||
}
|
||||
});
|
||||
|
||||
const secrets = await secretDAL.rawFind(
|
||||
{
|
||||
$in: {
|
||||
folderId: folders.map((folder) => folder.id)
|
||||
}
|
||||
},
|
||||
{ countDistinct: "key" }
|
||||
);
|
||||
|
||||
return { environmentCount: environments.length, secretCount: Number(secrets?.[0]?.count ?? 0) };
|
||||
};
|
||||
|
||||
return {
|
||||
createSecret,
|
||||
deleteSecret,
|
||||
@ -2933,6 +2967,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
getSecretsByFolderMappings,
|
||||
getSecretById,
|
||||
getAccessibleSecrets,
|
||||
getSecretVersionsByIds
|
||||
getSecretVersionsByIds,
|
||||
getProjectSecretResourcesCount
|
||||
};
|
||||
};
|
||||
|
@ -3339,6 +3339,20 @@ export const secretServiceFactory = ({
|
||||
}));
|
||||
};
|
||||
|
||||
const getProjectSecretResourcesCount = async (projectId: string, actor: OrgServiceActor) => {
|
||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
|
||||
if (!shouldUseSecretV2Bridge)
|
||||
throw new BadRequestError({
|
||||
message: "Project version does not support pagination",
|
||||
name: "PaginationNotSupportedError"
|
||||
});
|
||||
|
||||
const count = await secretV2BridgeService.getProjectSecretResourcesCount(projectId, actor);
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
return {
|
||||
attachTags,
|
||||
detachTags,
|
||||
@ -3371,6 +3385,7 @@ export const secretServiceFactory = ({
|
||||
getSecretByIdRaw,
|
||||
getAccessibleSecrets,
|
||||
getSecretVersionsV2ByIds,
|
||||
getChangeVersions
|
||||
getChangeVersions,
|
||||
getProjectSecretResourcesCount
|
||||
};
|
||||
};
|
||||
|
1
frontend/public/lotties/home.json
Normal file
1
frontend/public/lotties/home.json
Normal file
File diff suppressed because one or more lines are too long
@ -59,15 +59,18 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
|
||||
|
||||
return project;
|
||||
};
|
||||
export const getProjectHomePage = (type: ProjectType) => {
|
||||
switch (type) {
|
||||
case ProjectType.CertificateManager:
|
||||
return `/projects/$projectId/${type}/subscribers` as const;
|
||||
case ProjectType.SecretScanning:
|
||||
return `/projects/$projectId/${type}/data-sources` as const;
|
||||
default:
|
||||
return `/projects/$projectId/${type}/overview` as const;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const getProjectHomePage = (_type: ProjectType) => {
|
||||
return "/projects/$projectId/overview";
|
||||
|
||||
// switch (type) {
|
||||
// case ProjectType.CertificateManager:
|
||||
// return `/projects/$projectId/${type}/subscribers` as const;
|
||||
// case ProjectType.SecretScanning:
|
||||
// return `/projects/$projectId/${type}/data-sources` as const;
|
||||
// default:
|
||||
// return `/projects/$projectId/${type}/overview` as const;
|
||||
// }
|
||||
};
|
||||
|
||||
export const getProjectTitle = (type: ProjectType) => {
|
||||
|
@ -15,7 +15,9 @@ import {
|
||||
TGetDashboardProjectSecretsByKeys,
|
||||
TGetDashboardProjectSecretsDetailsDTO,
|
||||
TGetDashboardProjectSecretsOverviewDTO,
|
||||
TGetDashboardProjectSecretsQuickSearchDTO
|
||||
TGetDashboardProjectSecretsQuickSearchDTO,
|
||||
TGetProjectOverview,
|
||||
TProjectOverview
|
||||
} from "@app/hooks/api/dashboard/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
|
||||
@ -72,7 +74,13 @@ export const dashboardKeys = {
|
||||
...dashboardKeys.all(),
|
||||
"accessible-secrets",
|
||||
{ projectId, secretPath, environment, filterByAction }
|
||||
] as const
|
||||
] as const,
|
||||
getProjectOverview: ({ projectId, projectSlug }: TGetProjectOverview) => [
|
||||
...dashboardKeys.all(),
|
||||
"project-overview",
|
||||
projectId,
|
||||
projectSlug
|
||||
]
|
||||
};
|
||||
|
||||
export const fetchProjectSecretsOverview = async ({
|
||||
@ -464,3 +472,35 @@ export const useGetAccessibleSecrets = ({
|
||||
fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction, recursive })
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetProjectOverview = (
|
||||
{ projectId, projectSlug }: TGetProjectOverview,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TProjectOverview,
|
||||
unknown,
|
||||
TProjectOverview,
|
||||
ReturnType<typeof dashboardKeys.getProjectOverview>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
...options,
|
||||
refetchOnMount: "always",
|
||||
queryKey: dashboardKeys.getProjectOverview({
|
||||
projectId,
|
||||
projectSlug
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TProjectOverview>(
|
||||
"/api/v1/dashboard/project-overview",
|
||||
{
|
||||
params: { projectId, projectSlug }
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -156,3 +156,39 @@ export type TGetAccessibleSecretsDTO = {
|
||||
| ProjectPermissionSecretActions.DescribeSecret
|
||||
| ProjectPermissionSecretActions.ReadValue;
|
||||
};
|
||||
|
||||
export type TGetProjectOverview = {
|
||||
projectId: string;
|
||||
projectSlug: string;
|
||||
};
|
||||
|
||||
export type TProjectOverview = {
|
||||
accessControl: {
|
||||
userCount: number;
|
||||
machineIdentityCount: number;
|
||||
groupCount: number;
|
||||
};
|
||||
secretsManagement: {
|
||||
secretCount: number;
|
||||
environmentCount: number;
|
||||
pendingApprovalCount: number;
|
||||
};
|
||||
certificateManagement: {
|
||||
internalCaCount: number;
|
||||
externalCaCount: number;
|
||||
expiryCount: number;
|
||||
};
|
||||
kms: {
|
||||
keyCount: number;
|
||||
kmipClientCount: number;
|
||||
};
|
||||
ssh: {
|
||||
hostCount: number;
|
||||
hostGroupCount: number;
|
||||
};
|
||||
secretScanning: {
|
||||
dataSourceCount: number;
|
||||
resourceCount: number;
|
||||
findingCount: number;
|
||||
};
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ export const ProjectGeneralLayout = () => {
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div className="border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
|
||||
Project Overview
|
||||
Project Controls
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Menu>
|
||||
|
@ -55,7 +55,7 @@ export const ProjectLayout = () => {
|
||||
const isKms = currentProductType === ProjectType.KMS;
|
||||
const isSsh = currentProductType === ProjectType.SSH;
|
||||
const isSecretScanning = currentProductType === ProjectType.SecretScanning;
|
||||
|
||||
const isOverview = !currentProductType;
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@ -83,6 +83,28 @@ export const ProjectLayout = () => {
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col justify-between">
|
||||
<Menu>
|
||||
<ShouldWrap
|
||||
wrapper={Tooltip}
|
||||
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
|
||||
content="Project Overview"
|
||||
position="right"
|
||||
>
|
||||
<Link
|
||||
to="/projects/$projectId/overview"
|
||||
params={{ projectId: currentWorkspace.id }}
|
||||
>
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden rounded-none"
|
||||
isSelected={isOverview}
|
||||
leftIcon={<Lottie className="inline-block h-6 w-6 shrink-0" icon="home" />}
|
||||
>
|
||||
{isOverview && (
|
||||
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
|
||||
)}
|
||||
Overview
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</ShouldWrap>
|
||||
<ShouldWrap
|
||||
wrapper={Tooltip}
|
||||
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
|
||||
|
@ -1,12 +1,35 @@
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Menu, MenuItem } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { Badge, Menu, MenuItem } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretScanningFindingActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useGetSecretScanningUnresolvedFindingCount } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
export const SecretScanningLayout = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount(
|
||||
currentWorkspace.id,
|
||||
{
|
||||
enabled:
|
||||
subscription.secretScanning &&
|
||||
permission.can(
|
||||
ProjectPermissionSecretScanningFindingActions.Read,
|
||||
ProjectPermissionSub.SecretScanningFindings
|
||||
),
|
||||
refetchInterval: 30000
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
@ -38,7 +61,18 @@ export const SecretScanningLayout = () => {
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <MenuItem isSelected={isActive}>Findings</MenuItem>}
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span>Findings</span>
|
||||
{Boolean(unresolvedFindings) && (
|
||||
<Badge variant="primary" className="mr-2">
|
||||
{unresolvedFindings}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/$projectId/secret-scanning/settings"
|
||||
|
318
frontend/src/pages/project/OverviewPage/OverviewPage.tsx
Normal file
318
frontend/src/pages/project/OverviewPage/OverviewPage.tsx
Normal file
@ -0,0 +1,318 @@
|
||||
import { Fragment } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import {
|
||||
faArrowRightArrowLeft,
|
||||
faArrowUpRightFromSquare,
|
||||
faBook,
|
||||
faClock,
|
||||
faCode,
|
||||
faCopy,
|
||||
faExpand,
|
||||
faRotate,
|
||||
faSearch,
|
||||
faServer,
|
||||
faShield,
|
||||
faTerminal,
|
||||
faUser,
|
||||
faUserGroup,
|
||||
faWarning
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ContentLoader, IconButton, PageHeader } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetProjectOverview } from "@app/hooks/api/dashboard/queries";
|
||||
import { ProductCard } from "@app/pages/project/OverviewPage/components/ProductCard";
|
||||
|
||||
const DocLinks = [
|
||||
{
|
||||
label: "API Docs",
|
||||
description: "API endpoints, authentication, and examples",
|
||||
icon: faBook,
|
||||
link: "https://infisical.com/docs/api-reference/overview/introduction"
|
||||
},
|
||||
{
|
||||
label: "Access Control",
|
||||
description: "Manage user permissions and resource-level security",
|
||||
icon: faShield,
|
||||
link: "https://infisical.com/docs/documentation/platform/access-controls/overview"
|
||||
},
|
||||
{
|
||||
label: "CLI",
|
||||
description: "Install, configure, and use command-line tools",
|
||||
icon: faTerminal,
|
||||
link: "https://infisical.com/docs/cli/overview"
|
||||
},
|
||||
{
|
||||
label: "SDKs",
|
||||
description: "Libraries and SDKs for popular programming languages",
|
||||
icon: faCode,
|
||||
link: "https://infisical.com/docs/sdks/overview"
|
||||
},
|
||||
{
|
||||
label: "Machine Identities",
|
||||
description: "Configure service accounts for automated workflows",
|
||||
icon: faServer,
|
||||
link: "https://infisical.com/docs/documentation/platform/identities/machine-identities#machine-identities"
|
||||
},
|
||||
{
|
||||
label: "Secret Syncs",
|
||||
description: "Automatically sync secrets to external platforms",
|
||||
icon: faArrowRightArrowLeft,
|
||||
link: "https://infisical.com/docs/integrations/secret-syncs/overview"
|
||||
},
|
||||
{
|
||||
label: "Secret Scanning",
|
||||
description: "Detect and prevent secret exposure in code",
|
||||
icon: faExpand,
|
||||
link: "https://infisical.com/docs/documentation/platform/secret-scanning/overview"
|
||||
},
|
||||
{
|
||||
label: "Secret Rotation",
|
||||
description: "Automate periodic secret updates and rotation",
|
||||
icon: faRotate,
|
||||
link: "https://infisical.com/docs/documentation/platform/secret-rotation/overview"
|
||||
}
|
||||
];
|
||||
|
||||
export const OverviewPage = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const {
|
||||
data: overview = {
|
||||
accessControl: { userCount: 0, machineIdentityCount: 0, groupCount: 0 },
|
||||
secretsManagement: { secretCount: 0, environmentCount: 0, pendingApprovalCount: 0 },
|
||||
certificateManagement: {
|
||||
internalCaCount: 0,
|
||||
externalCaCount: 0,
|
||||
expiryCount: 0
|
||||
},
|
||||
ssh: { hostCount: 0, hostGroupCount: 0 },
|
||||
kms: { keyCount: 0, kmipClientCount: 0 },
|
||||
secretScanning: {
|
||||
dataSourceCount: 0,
|
||||
resourceCount: 0,
|
||||
findingCount: 0
|
||||
}
|
||||
},
|
||||
isPending
|
||||
} = useGetProjectOverview({
|
||||
projectId: currentWorkspace.id,
|
||||
projectSlug: currentWorkspace.slug
|
||||
});
|
||||
|
||||
if (isPending) return <ContentLoader />;
|
||||
|
||||
const { secretsManagement, certificateManagement, kms, ssh, secretScanning } = overview;
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 pt-8">
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>Project Overview | {currentWorkspace.name}</title>
|
||||
</Helmet>
|
||||
<div className="flex w-full max-w-7xl flex-col justify-evenly">
|
||||
<PageHeader
|
||||
title={
|
||||
<div>
|
||||
<span>{currentWorkspace.name}</span>
|
||||
<div className="flex w-full flex-wrap items-center gap-2 text-xs font-normal">
|
||||
<div className="flex items-center gap-1 normal-case text-mineshaft-300">
|
||||
<span>Project Slug: {currentWorkspace.slug}</span>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(currentWorkspace.slug);
|
||||
createNotification({
|
||||
text: "Project slug copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
variant="plain"
|
||||
size="xs"
|
||||
ariaLabel="Copy project slug"
|
||||
>
|
||||
<FontAwesomeIcon className="text-mineshaft-400" icon={faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<span className="text-mineshaft-400">|</span>
|
||||
<div className="flex items-center gap-1 normal-case text-mineshaft-300">
|
||||
<span>Project ID: {currentWorkspace.id}</span>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(currentWorkspace.id);
|
||||
createNotification({
|
||||
text: "Project ID copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
variant="plain"
|
||||
size="xs"
|
||||
ariaLabel="Copy project ID"
|
||||
>
|
||||
<FontAwesomeIcon className="text-mineshaft-400" icon={faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="mb-3 mt-auto flex flex-col gap-4 lg:flex-row lg:items-center">
|
||||
{[
|
||||
{ icon: faUser, count: overview.accessControl.userCount, label: "Members" },
|
||||
{
|
||||
icon: faServer,
|
||||
count: overview.accessControl.machineIdentityCount,
|
||||
label: "Machine Identities"
|
||||
},
|
||||
{ icon: faUserGroup, count: overview.accessControl.groupCount, label: "Groups" }
|
||||
].map(({ icon, count, label }, index) => (
|
||||
<Fragment key={`${index + 1}-label`}>
|
||||
<div className="flex items-center gap-1.5 whitespace-nowrap text-sm text-mineshaft-300">
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
<span>{count}</span>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
<span className="hidden text-sm text-mineshaft-400 last:hidden lg:block">|</span>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div className="w-full border-t border-mineshaft-600" />
|
||||
<div className="flex flex-col">
|
||||
<div className="mt-16">
|
||||
<h3 className="mb-4 text-2xl">Products</h3>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<ProductCard
|
||||
title="Secrets Management"
|
||||
lottie="vault"
|
||||
to="/projects/$projectId/secret-manager/overview"
|
||||
items={[
|
||||
{ label: `${secretsManagement.secretCount} Secrets`, key: "secretCount" },
|
||||
{
|
||||
label: `${secretsManagement.environmentCount} Environments`,
|
||||
key: "environmentCount"
|
||||
}
|
||||
]}
|
||||
badgeProps={
|
||||
secretsManagement.pendingApprovalCount
|
||||
? {
|
||||
variant: "primary",
|
||||
icon: faClock,
|
||||
label: `${secretsManagement.pendingApprovalCount} Approval${secretsManagement.pendingApprovalCount > 1 ? "s" : ""}`,
|
||||
tooltipContent: `${secretsManagement.pendingApprovalCount} Pending Approval${secretsManagement.pendingApprovalCount > 1 ? "s" : ""}`
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<ProductCard
|
||||
title="Certificate Management"
|
||||
lottie="note"
|
||||
to="/projects/$projectId/cert-manager/subscribers"
|
||||
items={[
|
||||
{
|
||||
label: `${certificateManagement.internalCaCount} Internal CAs`,
|
||||
key: "internalCaCount"
|
||||
},
|
||||
{
|
||||
label: `${certificateManagement.externalCaCount} External CAs`,
|
||||
key: "externalCaCount"
|
||||
}
|
||||
]}
|
||||
badgeProps={
|
||||
certificateManagement.expiryCount
|
||||
? {
|
||||
variant: "primary",
|
||||
icon: faWarning,
|
||||
label: `${certificateManagement.expiryCount} Expiring`,
|
||||
tooltipContent: `${certificateManagement.expiryCount} Certificate${certificateManagement.expiryCount > 1 ? "s" : ""} Are About To Expire`
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<ProductCard
|
||||
title="KMS"
|
||||
lottie="unlock"
|
||||
to="/projects/$projectId/kms/overview"
|
||||
items={[
|
||||
{ label: `${kms.keyCount} Keys`, key: "keyCount" },
|
||||
{
|
||||
label: `${kms.kmipClientCount} KMIP Clients`,
|
||||
key: "kmipClientCount"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<ProductCard
|
||||
title="SSH"
|
||||
lottie="terminal"
|
||||
to="/projects/$projectId/ssh/overview"
|
||||
items={[
|
||||
{ label: `${ssh.hostCount} Hosts`, key: "hostCount" },
|
||||
{
|
||||
label: `${ssh.hostGroupCount} Host Groups`,
|
||||
key: "hostGroupCount"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<ProductCard
|
||||
title="Secret Scanning"
|
||||
lottie="secret-scan"
|
||||
to="/projects/$projectId/secret-scanning/data-sources"
|
||||
items={[
|
||||
{
|
||||
label: `${secretScanning.dataSourceCount} Data Sources`,
|
||||
key: "dataSourceCount"
|
||||
},
|
||||
{
|
||||
label: `${secretScanning.resourceCount} Resources`,
|
||||
key: "resourceCount"
|
||||
}
|
||||
]}
|
||||
badgeProps={
|
||||
secretScanning.findingCount
|
||||
? {
|
||||
variant: "primary",
|
||||
icon: faSearch,
|
||||
label: `${secretScanning.findingCount} Finding${secretScanning.findingCount > 1 ? "s" : ""}`,
|
||||
tooltipContent: `${secretScanning.findingCount} Unresolved Finding${secretScanning.findingCount > 1 ? "s" : ""}`
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-20">
|
||||
<h3 className="mb-4 text-xl">Documentation</h3>
|
||||
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
{DocLinks.map(({ label, description, icon, link }) => (
|
||||
<a
|
||||
key={label}
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rounded border border-mineshaft-600 bg-mineshaft-800 p-4 transition-transform duration-100 hover:scale-[103%] hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="w-full items-start">
|
||||
<div className="flex w-full items-center">
|
||||
<FontAwesomeIcon className="mr-2 text-bunker-200" icon={icon} />
|
||||
<span>{label}</span>
|
||||
<FontAwesomeIcon
|
||||
className="ml-auto text-bunker-400"
|
||||
size="sm"
|
||||
icon={faArrowUpRightFromSquare}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-bunker-300">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,63 @@
|
||||
import { Fragment } from "react";
|
||||
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, LinkProps } from "@tanstack/react-router";
|
||||
|
||||
import { Badge, Lottie, Tooltip } from "@app/components/v2";
|
||||
import { BadgeProps } from "@app/components/v2/Badge/Badge";
|
||||
|
||||
type Props = {
|
||||
to: LinkProps["to"];
|
||||
lottie: string;
|
||||
title: string;
|
||||
badgeProps?: {
|
||||
variant: BadgeProps["variant"];
|
||||
label: string;
|
||||
tooltipContent: string;
|
||||
icon: IconDefinition;
|
||||
};
|
||||
items: {
|
||||
key: string;
|
||||
label: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const ProductCard = ({ to, items, title, badgeProps, lottie }: Props) => {
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
className="overflow-clip rounded border border-l-[4px] border-mineshaft-600 border-l-primary/75 bg-mineshaft-800 p-4 transition-transform duration-100 hover:scale-[103%] hover:border-l-primary hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<div className="rounded border border-mineshaft-500 bg-mineshaft-600 p-1.5 shadow-inner">
|
||||
<Lottie className="h-[1.75rem] w-[1.75rem] shrink-0" icon={lottie} />
|
||||
</div>
|
||||
<div className="-mt-0.5 flex w-full flex-col">
|
||||
<div className="flex w-full items-center">
|
||||
<span className="text-xl">{title}</span>
|
||||
{badgeProps && (
|
||||
<Tooltip className="max-w-sm" content={badgeProps.tooltipContent}>
|
||||
<div className="ml-auto">
|
||||
<Badge className="mt-0.5 flex items-center gap-1.5" variant={badgeProps.variant}>
|
||||
<FontAwesomeIcon className="text-yellow" icon={badgeProps.icon} />
|
||||
<span>{badgeProps.label}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="-mt-0.5 flex items-center gap-2">
|
||||
{items.map((item) => (
|
||||
<Fragment key={item.key}>
|
||||
<div className="flex items-center gap-1.5 whitespace-nowrap text-sm text-mineshaft-300">
|
||||
{item.label}
|
||||
</div>
|
||||
<span className="text-sm text-mineshaft-400 last:hidden">|</span>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
19
frontend/src/pages/project/OverviewPage/route.tsx
Normal file
19
frontend/src/pages/project/OverviewPage/route.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { OverviewPage } from "./OverviewPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/overview"
|
||||
)({
|
||||
component: OverviewPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Overview"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
@ -18,7 +18,7 @@ export const SettingsPage = () => {
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title={t("settings.project.title")}
|
||||
description="Configure your project details and project-specific settings"
|
||||
description="Configure your project details and project-specific settings"
|
||||
/>
|
||||
<Tabs defaultValue={tabs[0].key}>
|
||||
<TabList>
|
||||
|
@ -69,6 +69,7 @@ import { Route as organizationSecretSharingPageRouteImport } from './pages/organ
|
||||
import { Route as organizationGatewaysGatewayListPageRouteImport } from './pages/organization/Gateways/GatewayListPage/route'
|
||||
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
|
||||
import { Route as projectLayoutGeneralImport } from './pages/project/layout-general'
|
||||
import { Route as projectOverviewPageRouteImport } from './pages/project/OverviewPage/route'
|
||||
import { Route as organizationSettingsPageOauthCallbackPageRouteImport } from './pages/organization/SettingsPage/OauthCallbackPage/route'
|
||||
import { Route as sshLayoutImport } from './pages/ssh/layout'
|
||||
import { Route as secretScanningLayoutImport } from './pages/secret-scanning/layout'
|
||||
@ -810,6 +811,12 @@ const projectLayoutGeneralRoute = projectLayoutGeneralImport.update({
|
||||
getParentRoute: () => projectLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const projectOverviewPageRouteRoute = projectOverviewPageRouteImport.update({
|
||||
id: '/overview',
|
||||
path: '/overview',
|
||||
getParentRoute: () => projectLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const organizationSettingsPageOauthCallbackPageRouteRoute =
|
||||
organizationSettingsPageOauthCallbackPageRouteImport.update({
|
||||
id: '/oauth/callback',
|
||||
@ -2348,6 +2355,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof organizationSettingsPageOauthCallbackPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/overview': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/overview'
|
||||
path: '/overview'
|
||||
fullPath: '/projects/$projectId/overview'
|
||||
preLoaderRoute: typeof projectOverviewPageRouteImport
|
||||
parentRoute: typeof projectLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout'
|
||||
path: ''
|
||||
@ -4053,6 +4067,7 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutSshRout
|
||||
)
|
||||
|
||||
interface projectLayoutRouteChildren {
|
||||
projectOverviewPageRouteRoute: typeof projectOverviewPageRouteRoute
|
||||
projectLayoutGeneralRoute: typeof projectLayoutGeneralRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
|
||||
@ -4063,6 +4078,7 @@ interface projectLayoutRouteChildren {
|
||||
}
|
||||
|
||||
const projectLayoutRouteChildren: projectLayoutRouteChildren = {
|
||||
projectOverviewPageRouteRoute: projectOverviewPageRouteRoute,
|
||||
projectLayoutGeneralRoute: projectLayoutGeneralRouteWithChildren,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren,
|
||||
@ -4371,6 +4387,7 @@ export interface FileRoutesByFullPath {
|
||||
'/admin/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute
|
||||
'/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute
|
||||
'/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
|
||||
'/projects/$projectId/overview': typeof projectOverviewPageRouteRoute
|
||||
'/projects/$projectId/cert-manager': typeof certManagerLayoutRouteWithChildren
|
||||
'/projects/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
|
||||
'/projects/$projectId/kms': typeof kmsLayoutRouteWithChildren
|
||||
@ -4564,6 +4581,7 @@ export interface FileRoutesByTo {
|
||||
'/admin/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute
|
||||
'/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute
|
||||
'/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
|
||||
'/projects/$projectId/overview': typeof projectOverviewPageRouteRoute
|
||||
'/projects/$projectId/cert-manager': typeof certManagerLayoutRouteWithChildren
|
||||
'/projects/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
|
||||
'/projects/$projectId/kms': typeof kmsLayoutRouteWithChildren
|
||||
@ -4766,6 +4784,7 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout': typeof projectLayoutRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/overview': typeof projectOverviewPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout': typeof projectLayoutGeneralRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
|
||||
@ -4975,6 +4994,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/resources/user-identities'
|
||||
| '/secret-manager/$projectId/approval'
|
||||
| '/organization/settings/oauth/callback'
|
||||
| '/projects/$projectId/overview'
|
||||
| '/projects/$projectId/cert-manager'
|
||||
| '/projects/$projectId/integrations'
|
||||
| '/projects/$projectId/kms'
|
||||
@ -5167,6 +5187,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/resources/user-identities'
|
||||
| '/secret-manager/$projectId/approval'
|
||||
| '/organization/settings/oauth/callback'
|
||||
| '/projects/$projectId/overview'
|
||||
| '/projects/$projectId/cert-manager'
|
||||
| '/projects/$projectId/integrations'
|
||||
| '/projects/$projectId/kms'
|
||||
@ -5367,6 +5388,7 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/overview'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations'
|
||||
@ -5903,6 +5925,7 @@ export const routeTree = rootRoute
|
||||
"filePath": "project/layout.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/$projectId",
|
||||
"children": [
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/overview",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager",
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations",
|
||||
@ -5920,6 +5943,10 @@ export const routeTree = rootRoute
|
||||
"filePath": "organization/SettingsPage/OauthCallbackPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/settings"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/overview": {
|
||||
"filePath": "project/OverviewPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout": {
|
||||
"filePath": "project/layout-general.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout",
|
||||
|
@ -392,6 +392,7 @@ export const routes = rootRoute("root.tsx", [
|
||||
]),
|
||||
route("/projects/$projectId", [
|
||||
layout("project-layout", "project/layout.tsx", [
|
||||
route("/overview", "project/OverviewPage/route.tsx"),
|
||||
projectGeneralRoutes,
|
||||
secretManagerRoutes,
|
||||
secretManagerIntegrationsRedirect,
|
||||
|
Reference in New Issue
Block a user