mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-11 12:11:38 +00:00
Compare commits
2 Commits
daniel/pod
...
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 { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
import { isValidIp } from "@app/lib/ip";
|
import { isValidIp } from "@app/lib/ip";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||||
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
|
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
|
||||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
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 {
|
return {
|
||||||
createKmipClient,
|
createKmipClient,
|
||||||
updateKmipClient,
|
updateKmipClient,
|
||||||
@ -806,6 +827,7 @@ export const kmipServiceFactory = ({
|
|||||||
generateOrgKmipServerCertificate,
|
generateOrgKmipServerCertificate,
|
||||||
getOrgKmip,
|
getOrgKmip,
|
||||||
getServerCertificateBySerialNumber,
|
getServerCertificateBySerialNumber,
|
||||||
registerServer
|
registerServer,
|
||||||
|
getProjectClientCount
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -437,6 +437,7 @@ export const secretScanningV2DALFactory = (db: TDbClient) => {
|
|||||||
return {
|
return {
|
||||||
dataSources: {
|
dataSources: {
|
||||||
...dataSourceOrm,
|
...dataSourceOrm,
|
||||||
|
findRaw: dataSourceOrm.find,
|
||||||
find: findDataSource,
|
find: findDataSource,
|
||||||
findById: findDataSourceById,
|
findById: findDataSourceById,
|
||||||
findOne: findOneDataSource,
|
findOne: findOneDataSource,
|
||||||
|
@ -881,6 +881,47 @@ export const secretScanningV2ServiceFactory = ({
|
|||||||
return config;
|
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 {
|
return {
|
||||||
listSecretScanningDataSourceOptions,
|
listSecretScanningDataSourceOptions,
|
||||||
listSecretScanningDataSourcesByProjectId,
|
listSecretScanningDataSourcesByProjectId,
|
||||||
@ -900,6 +941,7 @@ export const secretScanningV2ServiceFactory = ({
|
|||||||
updateSecretScanningFindingById,
|
updateSecretScanningFindingById,
|
||||||
findSecretScanningConfigByProjectId,
|
findSecretScanningConfigByProjectId,
|
||||||
upsertSecretScanningConfig,
|
upsertSecretScanningConfig,
|
||||||
|
getProjectResourcesCount,
|
||||||
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue),
|
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue),
|
||||||
bitbucket: bitbucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService)
|
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 { 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 { TSshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-membership-dal";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
@ -414,6 +415,26 @@ export const sshHostGroupServiceFactory = ({
|
|||||||
return { sshHostGroup, sshHost };
|
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 {
|
return {
|
||||||
createSshHostGroup,
|
createSshHostGroup,
|
||||||
getSshHostGroup,
|
getSshHostGroup,
|
||||||
@ -421,6 +442,7 @@ export const sshHostGroupServiceFactory = ({
|
|||||||
updateSshHostGroup,
|
updateSshHostGroup,
|
||||||
listSshHostGroupHosts,
|
listSshHostGroupHosts,
|
||||||
addHostToSshHostGroup,
|
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 { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||||
import { PgSqlLock } from "@app/keystore/keystore";
|
import { PgSqlLock } from "@app/keystore/keystore";
|
||||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
@ -60,6 +61,7 @@ type TSshHostServiceFactoryDep = {
|
|||||||
| "findOne"
|
| "findOne"
|
||||||
| "findSshHostByIdWithLoginMappings"
|
| "findSshHostByIdWithLoginMappings"
|
||||||
| "findUserAccessibleSshHosts"
|
| "findUserAccessibleSshHosts"
|
||||||
|
| "find"
|
||||||
>;
|
>;
|
||||||
sshHostLoginUserDAL: TSshHostLoginUserDALFactory;
|
sshHostLoginUserDAL: TSshHostLoginUserDALFactory;
|
||||||
sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory;
|
sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory;
|
||||||
@ -637,6 +639,26 @@ export const sshHostServiceFactory = ({
|
|||||||
return publicKey;
|
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 {
|
return {
|
||||||
listSshHosts,
|
listSshHosts,
|
||||||
createSshHost,
|
createSshHost,
|
||||||
@ -646,6 +668,7 @@ export const sshHostServiceFactory = ({
|
|||||||
issueSshHostUserCert,
|
issueSshHostUserCert,
|
||||||
issueSshHostHostCert,
|
issueSshHostHostCert,
|
||||||
getSshHostUserCaPk,
|
getSshHostUserCaPk,
|
||||||
getSshHostHostCaPk
|
getSshHostHostCaPk,
|
||||||
|
getProjectHostCount
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -12,6 +12,11 @@ type TKnexDynamicPrimitiveOperator<T extends object> =
|
|||||||
operator: "notIn";
|
operator: "notIn";
|
||||||
value: string[];
|
value: string[];
|
||||||
field: Extract<keyof T, string>;
|
field: Extract<keyof T, string>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
operator: "lte";
|
||||||
|
value: Date;
|
||||||
|
field: Extract<keyof T, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TKnexDynamicInOperator<T extends object> = {
|
type TKnexDynamicInOperator<T extends object> = {
|
||||||
@ -82,6 +87,10 @@ export const buildDynamicKnexQuery = <T extends object>(
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "lte": {
|
||||||
|
void queryBuilder.where(filterAst.field, "<=", filterAst.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new UnauthorizedError({ message: `Invalid knex dynamic operator: ${filterAst.operator}` });
|
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 { BadRequestError } from "@app/lib/errors";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
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 { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@ -1354,4 +1354,128 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
return { secrets };
|
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"
|
| "findByIdWithAssociatedCa"
|
||||||
| "findWithAssociatedCa"
|
| "findWithAssociatedCa"
|
||||||
| "findByNameAndProjectIdWithAssociatedCa"
|
| "findByNameAndProjectIdWithAssociatedCa"
|
||||||
|
| "find"
|
||||||
>;
|
>;
|
||||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update" | "find">;
|
||||||
internalCertificateAuthorityService: TInternalCertificateAuthorityServiceFactory;
|
internalCertificateAuthorityService: TInternalCertificateAuthorityServiceFactory;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
@ -382,11 +383,36 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
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 {
|
return {
|
||||||
createCertificateAuthority,
|
createCertificateAuthority,
|
||||||
findCertificateAuthorityByNameAndProjectId,
|
findCertificateAuthorityByNameAndProjectId,
|
||||||
listCertificateAuthoritiesByProjectId,
|
listCertificateAuthoritiesByProjectId,
|
||||||
updateCertificateAuthority,
|
updateCertificateAuthority,
|
||||||
deleteCertificateAuthority
|
deleteCertificateAuthority,
|
||||||
|
getProjectCertificateAuthorityCount
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-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 {
|
return {
|
||||||
getCert,
|
getCert,
|
||||||
getCertPrivateKey,
|
getCertPrivateKey,
|
||||||
@ -607,6 +640,7 @@ export const certificateServiceFactory = ({
|
|||||||
revokeCert,
|
revokeCert,
|
||||||
getCertBody,
|
getCertBody,
|
||||||
importCert,
|
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 {
|
return {
|
||||||
createCmek,
|
createCmek,
|
||||||
updateCmekById,
|
updateCmekById,
|
||||||
@ -387,6 +407,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
|||||||
cmekSign,
|
cmekSign,
|
||||||
cmekVerify,
|
cmekVerify,
|
||||||
listSigningAlgorithms,
|
listSigningAlgorithms,
|
||||||
getPublicKey
|
getPublicKey,
|
||||||
|
getProjectKeyCount
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -13,6 +13,7 @@ import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
|||||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { isUuidV4 } from "@app/lib/validator";
|
import { isUuidV4 } from "@app/lib/validator";
|
||||||
|
|
||||||
import { TGroupDALFactory } from "../../ee/services/group/group-dal";
|
import { TGroupDALFactory } from "../../ee/services/group/group-dal";
|
||||||
@ -33,7 +34,10 @@ import {
|
|||||||
} from "./group-project-types";
|
} from "./group-project-types";
|
||||||
|
|
||||||
type TGroupProjectServiceFactoryDep = {
|
type TGroupProjectServiceFactoryDep = {
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "findOne" | "transaction" | "create" | "delete" | "findByProjectId">;
|
groupProjectDAL: Pick<
|
||||||
|
TGroupProjectDALFactory,
|
||||||
|
"findOne" | "transaction" | "create" | "delete" | "findByProjectId" | "find"
|
||||||
|
>;
|
||||||
groupProjectMembershipRoleDAL: Pick<
|
groupProjectMembershipRoleDAL: Pick<
|
||||||
TGroupProjectMembershipRoleDALFactory,
|
TGroupProjectMembershipRoleDALFactory,
|
||||||
"create" | "transaction" | "insertMany" | "delete"
|
"create" | "transaction" | "insertMany" | "delete"
|
||||||
@ -508,12 +512,33 @@ export const groupProjectServiceFactory = ({
|
|||||||
return { users: members, totalCount };
|
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 {
|
return {
|
||||||
addGroupToProject,
|
addGroupToProject,
|
||||||
updateGroupInProject,
|
updateGroupInProject,
|
||||||
removeGroupFromProject,
|
removeGroupFromProject,
|
||||||
listGroupsInProject,
|
listGroupsInProject,
|
||||||
getGroupInProject,
|
getGroupInProject,
|
||||||
listProjectGroupUsers
|
listProjectGroupUsers,
|
||||||
|
getProjectGroupCount
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,7 @@ import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/
|
|||||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
@ -403,12 +404,33 @@ export const identityProjectServiceFactory = ({
|
|||||||
return identityMembership;
|
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 {
|
return {
|
||||||
createProjectIdentity,
|
createProjectIdentity,
|
||||||
updateProjectIdentity,
|
updateProjectIdentity,
|
||||||
deleteProjectIdentity,
|
deleteProjectIdentity,
|
||||||
listProjectIdentities,
|
listProjectIdentities,
|
||||||
getProjectIdentityByIdentityId,
|
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 { BadRequestError, ForbiddenRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
@ -567,6 +568,35 @@ export const projectMembershipServiceFactory = ({
|
|||||||
return deletedMembership;
|
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 {
|
return {
|
||||||
getProjectMemberships,
|
getProjectMemberships,
|
||||||
getProjectMembershipByUsername,
|
getProjectMembershipByUsername,
|
||||||
@ -575,6 +605,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
deleteProjectMembership, // TODO: Remove this
|
deleteProjectMembership, // TODO: Remove this
|
||||||
addUsersToProject,
|
addUsersToProject,
|
||||||
leaveProject,
|
leaveProject,
|
||||||
getProjectMembershipById
|
getProjectMembershipById,
|
||||||
|
getProjectMembershipCount
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -811,6 +811,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...secretOrm,
|
...secretOrm,
|
||||||
|
rawFind: secretOrm.find,
|
||||||
update,
|
update,
|
||||||
bulkUpdate,
|
bulkUpdate,
|
||||||
deleteMany,
|
deleteMany,
|
||||||
|
@ -26,6 +26,7 @@ import { diff, groupBy } from "@app/lib/fn";
|
|||||||
import { setKnexStringValue } from "@app/lib/knex";
|
import { setKnexStringValue } from "@app/lib/knex";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||||
@ -86,7 +87,7 @@ type TSecretV2BridgeServiceFactoryDep = {
|
|||||||
secretTagDAL: TSecretTagDALFactory;
|
secretTagDAL: TSecretTagDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs" | "find">;
|
||||||
folderDAL: Pick<
|
folderDAL: Pick<
|
||||||
TSecretFolderDALFactory,
|
TSecretFolderDALFactory,
|
||||||
| "findBySecretPath"
|
| "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 {
|
return {
|
||||||
createSecret,
|
createSecret,
|
||||||
deleteSecret,
|
deleteSecret,
|
||||||
@ -2933,6 +2967,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
getSecretsByFolderMappings,
|
getSecretsByFolderMappings,
|
||||||
getSecretById,
|
getSecretById,
|
||||||
getAccessibleSecrets,
|
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 {
|
return {
|
||||||
attachTags,
|
attachTags,
|
||||||
detachTags,
|
detachTags,
|
||||||
@ -3371,6 +3385,7 @@ export const secretServiceFactory = ({
|
|||||||
getSecretByIdRaw,
|
getSecretByIdRaw,
|
||||||
getAccessibleSecrets,
|
getAccessibleSecrets,
|
||||||
getSecretVersionsV2ByIds,
|
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;
|
return project;
|
||||||
};
|
};
|
||||||
export const getProjectHomePage = (type: ProjectType) => {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
switch (type) {
|
export const getProjectHomePage = (_type: ProjectType) => {
|
||||||
case ProjectType.CertificateManager:
|
return "/projects/$projectId/overview";
|
||||||
return `/projects/$projectId/${type}/subscribers` as const;
|
|
||||||
case ProjectType.SecretScanning:
|
// switch (type) {
|
||||||
return `/projects/$projectId/${type}/data-sources` as const;
|
// case ProjectType.CertificateManager:
|
||||||
default:
|
// return `/projects/$projectId/${type}/subscribers` as const;
|
||||||
return `/projects/$projectId/${type}/overview` 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) => {
|
export const getProjectTitle = (type: ProjectType) => {
|
||||||
|
@ -15,7 +15,9 @@ import {
|
|||||||
TGetDashboardProjectSecretsByKeys,
|
TGetDashboardProjectSecretsByKeys,
|
||||||
TGetDashboardProjectSecretsDetailsDTO,
|
TGetDashboardProjectSecretsDetailsDTO,
|
||||||
TGetDashboardProjectSecretsOverviewDTO,
|
TGetDashboardProjectSecretsOverviewDTO,
|
||||||
TGetDashboardProjectSecretsQuickSearchDTO
|
TGetDashboardProjectSecretsQuickSearchDTO,
|
||||||
|
TGetProjectOverview,
|
||||||
|
TProjectOverview
|
||||||
} from "@app/hooks/api/dashboard/types";
|
} from "@app/hooks/api/dashboard/types";
|
||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
|
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
|
||||||
@ -72,7 +74,13 @@ export const dashboardKeys = {
|
|||||||
...dashboardKeys.all(),
|
...dashboardKeys.all(),
|
||||||
"accessible-secrets",
|
"accessible-secrets",
|
||||||
{ projectId, secretPath, environment, filterByAction }
|
{ projectId, secretPath, environment, filterByAction }
|
||||||
] as const
|
] as const,
|
||||||
|
getProjectOverview: ({ projectId, projectSlug }: TGetProjectOverview) => [
|
||||||
|
...dashboardKeys.all(),
|
||||||
|
"project-overview",
|
||||||
|
projectId,
|
||||||
|
projectSlug
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchProjectSecretsOverview = async ({
|
export const fetchProjectSecretsOverview = async ({
|
||||||
@ -464,3 +472,35 @@ export const useGetAccessibleSecrets = ({
|
|||||||
fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction, recursive })
|
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.DescribeSecret
|
||||||
| ProjectPermissionSecretActions.ReadValue;
|
| 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]">
|
<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">
|
<div className="border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
|
||||||
Project Overview
|
Project Controls
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Menu>
|
<Menu>
|
||||||
|
@ -55,7 +55,7 @@ export const ProjectLayout = () => {
|
|||||||
const isKms = currentProductType === ProjectType.KMS;
|
const isKms = currentProductType === ProjectType.KMS;
|
||||||
const isSsh = currentProductType === ProjectType.SSH;
|
const isSsh = currentProductType === ProjectType.SSH;
|
||||||
const isSecretScanning = currentProductType === ProjectType.SecretScanning;
|
const isSecretScanning = currentProductType === ProjectType.SecretScanning;
|
||||||
|
const isOverview = !currentProductType;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@ -83,6 +83,28 @@ export const ProjectLayout = () => {
|
|||||||
>
|
>
|
||||||
<nav className="items-between flex h-full flex-col justify-between">
|
<nav className="items-between flex h-full flex-col justify-between">
|
||||||
<Menu>
|
<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
|
<ShouldWrap
|
||||||
wrapper={Tooltip}
|
wrapper={Tooltip}
|
||||||
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
|
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
|
||||||
|
@ -1,12 +1,35 @@
|
|||||||
import { Link, Outlet } from "@tanstack/react-router";
|
import { Link, Outlet } from "@tanstack/react-router";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import { Menu, MenuItem } from "@app/components/v2";
|
import { Badge, Menu, MenuItem } from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
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 = () => {
|
export const SecretScanningLayout = () => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
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 (
|
return (
|
||||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
<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">
|
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||||
@ -38,7 +61,18 @@ export const SecretScanningLayout = () => {
|
|||||||
projectId: currentWorkspace.id
|
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>
|
||||||
<Link
|
<Link
|
||||||
to="/projects/$projectId/secret-scanning/settings"
|
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">
|
<div className="w-full max-w-7xl">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t("settings.project.title")}
|
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}>
|
<Tabs defaultValue={tabs[0].key}>
|
||||||
<TabList>
|
<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 organizationGatewaysGatewayListPageRouteImport } from './pages/organization/Gateways/GatewayListPage/route'
|
||||||
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
|
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
|
||||||
import { Route as projectLayoutGeneralImport } from './pages/project/layout-general'
|
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 organizationSettingsPageOauthCallbackPageRouteImport } from './pages/organization/SettingsPage/OauthCallbackPage/route'
|
||||||
import { Route as sshLayoutImport } from './pages/ssh/layout'
|
import { Route as sshLayoutImport } from './pages/ssh/layout'
|
||||||
import { Route as secretScanningLayoutImport } from './pages/secret-scanning/layout'
|
import { Route as secretScanningLayoutImport } from './pages/secret-scanning/layout'
|
||||||
@ -810,6 +811,12 @@ const projectLayoutGeneralRoute = projectLayoutGeneralImport.update({
|
|||||||
getParentRoute: () => projectLayoutRoute,
|
getParentRoute: () => projectLayoutRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const projectOverviewPageRouteRoute = projectOverviewPageRouteImport.update({
|
||||||
|
id: '/overview',
|
||||||
|
path: '/overview',
|
||||||
|
getParentRoute: () => projectLayoutRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const organizationSettingsPageOauthCallbackPageRouteRoute =
|
const organizationSettingsPageOauthCallbackPageRouteRoute =
|
||||||
organizationSettingsPageOauthCallbackPageRouteImport.update({
|
organizationSettingsPageOauthCallbackPageRouteImport.update({
|
||||||
id: '/oauth/callback',
|
id: '/oauth/callback',
|
||||||
@ -2348,6 +2355,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof organizationSettingsPageOauthCallbackPageRouteImport
|
preLoaderRoute: typeof organizationSettingsPageOauthCallbackPageRouteImport
|
||||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsImport
|
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': {
|
'/_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'
|
id: '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout'
|
||||||
path: ''
|
path: ''
|
||||||
@ -4053,6 +4067,7 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutSshRout
|
|||||||
)
|
)
|
||||||
|
|
||||||
interface projectLayoutRouteChildren {
|
interface projectLayoutRouteChildren {
|
||||||
|
projectOverviewPageRouteRoute: typeof projectOverviewPageRouteRoute
|
||||||
projectLayoutGeneralRoute: typeof projectLayoutGeneralRouteWithChildren
|
projectLayoutGeneralRoute: typeof projectLayoutGeneralRouteWithChildren
|
||||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren
|
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren
|
||||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
|
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
|
||||||
@ -4063,6 +4078,7 @@ interface projectLayoutRouteChildren {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const projectLayoutRouteChildren: projectLayoutRouteChildren = {
|
const projectLayoutRouteChildren: projectLayoutRouteChildren = {
|
||||||
|
projectOverviewPageRouteRoute: projectOverviewPageRouteRoute,
|
||||||
projectLayoutGeneralRoute: projectLayoutGeneralRouteWithChildren,
|
projectLayoutGeneralRoute: projectLayoutGeneralRouteWithChildren,
|
||||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRoute:
|
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRoute:
|
||||||
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren,
|
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren,
|
||||||
@ -4371,6 +4387,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/admin/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute
|
'/admin/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute
|
||||||
'/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute
|
'/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute
|
||||||
'/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
|
'/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
|
||||||
|
'/projects/$projectId/overview': typeof projectOverviewPageRouteRoute
|
||||||
'/projects/$projectId/cert-manager': typeof certManagerLayoutRouteWithChildren
|
'/projects/$projectId/cert-manager': typeof certManagerLayoutRouteWithChildren
|
||||||
'/projects/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
|
'/projects/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
|
||||||
'/projects/$projectId/kms': typeof kmsLayoutRouteWithChildren
|
'/projects/$projectId/kms': typeof kmsLayoutRouteWithChildren
|
||||||
@ -4564,6 +4581,7 @@ export interface FileRoutesByTo {
|
|||||||
'/admin/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute
|
'/admin/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute
|
||||||
'/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute
|
'/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute
|
||||||
'/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
|
'/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
|
||||||
|
'/projects/$projectId/overview': typeof projectOverviewPageRouteRoute
|
||||||
'/projects/$projectId/cert-manager': typeof certManagerLayoutRouteWithChildren
|
'/projects/$projectId/cert-manager': typeof certManagerLayoutRouteWithChildren
|
||||||
'/projects/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
|
'/projects/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
|
||||||
'/projects/$projectId/kms': typeof kmsLayoutRouteWithChildren
|
'/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/projects/$projectId/_project-layout': typeof projectLayoutRouteWithChildren
|
||||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute
|
'/_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/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/_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/cert-manager': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren
|
||||||
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
|
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
|
||||||
@ -4975,6 +4994,7 @@ export interface FileRouteTypes {
|
|||||||
| '/admin/resources/user-identities'
|
| '/admin/resources/user-identities'
|
||||||
| '/secret-manager/$projectId/approval'
|
| '/secret-manager/$projectId/approval'
|
||||||
| '/organization/settings/oauth/callback'
|
| '/organization/settings/oauth/callback'
|
||||||
|
| '/projects/$projectId/overview'
|
||||||
| '/projects/$projectId/cert-manager'
|
| '/projects/$projectId/cert-manager'
|
||||||
| '/projects/$projectId/integrations'
|
| '/projects/$projectId/integrations'
|
||||||
| '/projects/$projectId/kms'
|
| '/projects/$projectId/kms'
|
||||||
@ -5167,6 +5187,7 @@ export interface FileRouteTypes {
|
|||||||
| '/admin/resources/user-identities'
|
| '/admin/resources/user-identities'
|
||||||
| '/secret-manager/$projectId/approval'
|
| '/secret-manager/$projectId/approval'
|
||||||
| '/organization/settings/oauth/callback'
|
| '/organization/settings/oauth/callback'
|
||||||
|
| '/projects/$projectId/overview'
|
||||||
| '/projects/$projectId/cert-manager'
|
| '/projects/$projectId/cert-manager'
|
||||||
| '/projects/$projectId/integrations'
|
| '/projects/$projectId/integrations'
|
||||||
| '/projects/$projectId/kms'
|
| '/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/projects/$projectId/_project-layout'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval'
|
| '/_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/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/_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/cert-manager'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations'
|
| '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations'
|
||||||
@ -5903,6 +5925,7 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "project/layout.tsx",
|
"filePath": "project/layout.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/$projectId",
|
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/$projectId",
|
||||||
"children": [
|
"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/_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/cert-manager",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations",
|
"/_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",
|
"filePath": "organization/SettingsPage/OauthCallbackPage/route.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/settings"
|
"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": {
|
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout": {
|
||||||
"filePath": "project/layout-general.tsx",
|
"filePath": "project/layout-general.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout",
|
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout",
|
||||||
|
@ -392,6 +392,7 @@ export const routes = rootRoute("root.tsx", [
|
|||||||
]),
|
]),
|
||||||
route("/projects/$projectId", [
|
route("/projects/$projectId", [
|
||||||
layout("project-layout", "project/layout.tsx", [
|
layout("project-layout", "project/layout.tsx", [
|
||||||
|
route("/overview", "project/OverviewPage/route.tsx"),
|
||||||
projectGeneralRoutes,
|
projectGeneralRoutes,
|
||||||
secretManagerRoutes,
|
secretManagerRoutes,
|
||||||
secretManagerIntegrationsRedirect,
|
secretManagerIntegrationsRedirect,
|
||||||
|
Reference in New Issue
Block a user