chore(frontend): changed source code to src folder from root

This commit is contained in:
akhilmhdh
2023-01-16 00:34:22 +05:30
parent c563548a1c
commit 63e9d83ba4
193 changed files with 280 additions and 33 deletions
@@ -0,0 +1,31 @@
import SecurityClient from '~/utilities/SecurityClient';
interface workspaceProps {
actionId: string;
}
/**
* This function fetches the data for a certain action performed by a user
* @param {object} obj
* @param {string} obj.actionId - id of an action for which we are trying to get data
* @returns
*/
const getActionData = async ({ actionId }: workspaceProps) => {
return SecurityClient.fetchCall(
'/api/v1/action/' + actionId, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json()).action;
} else {
console.log('Failed to get the info about an action');
}
});
};
export default getActionData;
@@ -0,0 +1,72 @@
import SecurityClient from '~/utilities/SecurityClient';
interface workspaceProps {
workspaceId: string;
offset: number;
limit: number;
userId: string;
actionNames: string;
}
/**
* This function fetches the activity logs for a certain project
* @param {object} obj
* @param {string} obj.workspaceId - workspace id for which we are trying to get project log
* @param {object} obj.offset - teh starting point of logs that we want to pull
* @param {object} obj.limit - how many logs will we output
* @param {object} obj.userId - optional userId filter - will only query logs for that user
* @param {string} obj.actionNames - optional actionNames filter - will only query logs for those actions
* @returns
*/
const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames }: workspaceProps) => {
let payload;
if (userId != "" && actionNames != '') {
payload = {
offset: String(offset),
limit: String(limit),
sortBy: 'recent',
userId: JSON.stringify(userId),
actionNames: actionNames
}
} else if (userId != "") {
payload = {
offset: String(offset),
limit: String(limit),
sortBy: 'recent',
userId: JSON.stringify(userId)
}
} else if (actionNames != "") {
payload = {
offset: String(offset),
limit: String(limit),
sortBy: 'recent',
actionNames: actionNames
}
} else {
payload = {
offset: String(offset),
limit: String(limit),
sortBy: 'recent'
}
}
return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + '/logs?' +
new URLSearchParams(payload),
{
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json()).logs;
} else {
console.log('Failed to get project logs');
}
});
};
export default getProjectLogs;
@@ -0,0 +1,39 @@
import SecurityClient from '~/utilities/SecurityClient';
interface workspaceProps {
workspaceId: string;
offset: number;
limit: number;
}
/**
* This function fetches the secret snapshots for a certain project
* @param {object} obj
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
* @param {object} obj.offset - teh starting point of snapshots that we want to pull
* @param {object} obj.limit - how many snapshots will we output
* @returns
*/
const getProjectSecretShanpshots = async ({ workspaceId, offset, limit }: workspaceProps) => {
return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + '/secret-snapshots?' +
new URLSearchParams({
offset: String(offset),
limit: String(limit)
}), {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json()).secretSnapshots;
} else {
console.log('Failed to get project secret snapshots');
}
});
};
export default getProjectSecretShanpshots;
@@ -0,0 +1,31 @@
import SecurityClient from '~/utilities/SecurityClient';
interface workspaceProps {
workspaceId: string;
}
/**
* This function fetches the count of secret snapshots for a certain project
* @param {object} obj
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
* @returns
*/
const getProjectSercetSnapshotsCount = async ({ workspaceId }: workspaceProps) => {
return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + '/secret-snapshots/count', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json()).count;
} else {
console.log('Failed to get the count of project secret snapshots');
}
});
};
export default getProjectSercetSnapshotsCount;
@@ -0,0 +1,31 @@
import SecurityClient from '~/utilities/SecurityClient';
interface SnapshotProps {
secretSnapshotId: string;
}
/**
* This function fetches the secrets for a certain secret snapshot
* @param {object} obj
* @param {string} obj.secretSnapshotId - snapshot id for which we are trying to get secrets
* @returns
*/
const getSecretSnapshotData = async ({ secretSnapshotId }: SnapshotProps) => {
return SecurityClient.fetchCall(
'/api/v1/secret-snapshot/' + secretSnapshotId, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json()).secretSnapshot;
} else {
console.log('Failed to get the secrets of a certain snapshot');
}
});
};
export default getSecretSnapshotData;
@@ -0,0 +1,40 @@
import SecurityClient from '~/utilities/SecurityClient';
interface secretVersionProps {
secretId: string;
offset: number;
limit: number;
}
/**
* This function fetches the versions of a specific secret
* @param {object} obj
* @param {string} obj.secretId - the id of a secret for which we are fetching the version history
* @param {number} obj.offset - the start of our query
* @param {number} obj.limit - how far our query goes
* @returns
*/
const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps) => {
return SecurityClient.fetchCall(
'/api/v1/secret/' + secretId + '/secret-versions?' +
new URLSearchParams({
offset: String(offset),
limit: String(limit)
}),
{
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
).then(async (res) => {
if (res && res.status == 200) {
return await res.json();
} else {
console.log('Failed to get secret version history');
}
});
};
export default getSecretVersions;
@@ -0,0 +1,30 @@
import SecurityClient from '~/utilities/SecurityClient';
/**
* This function performs a rollback of secrets in a certain project
* @param {object} obj
* @param {string} obj.workspaceId - id of the project for which we are rolling back data
* @param {number} obj.version - version to which we are rolling back
* @returns
*/
const performSecretRollback = async ({ workspaceId, version }: { workspaceId: string; version: number; }) => {
return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + "/secret-snapshots/rollback", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
version
})
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json());
} else {
console.log('Failed to perform the secret rollback');
}
});
};
export default performSecretRollback;
@@ -0,0 +1,185 @@
import { useEffect, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import getActionData from "@app/ee/api/secrets/GetActionData";
import patienceDiff from '@app/ee/utilities/findTextDifferences';
import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import DashboardInputField from '../../components/dashboard/DashboardInputField';
const {
decryptAssymmetric,
decryptSymmetric
} = require('../../components/utilities/cryptography/crypto');
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
interface SideBarProps {
toggleSidebar: (value: string) => void;
currentAction: string;
}
interface SecretProps {
secret: string;
secretKeyCiphertext: string;
secretKeyHash: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueHash: string;
secretValueIV: string;
secretValueTag: string;
}
interface DecryptedSecretProps {
newSecretVersion: {
key: string;
value: string;
}
oldSecretVersion: {
key: string;
value: string;
}
}
interface ActionProps {
name: string;
}
/**
* @param {object} obj
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
* @param {string} obj.currentAction - the action id for which a sidebar is being displayed
* @returns the sidebar with the payload of user activity logs
*/
const ActivitySideBar = ({
toggleSidebar,
currentAction
}: SideBarProps) => {
const { t } = useTranslation();
const router = useRouter();
const [actionData, setActionData] = useState<DecryptedSecretProps[]>();
const [actionMetaData, setActionMetaData] = useState<ActionProps>();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const getLogData = async () => {
setIsLoading(true);
const tempActionData = await getActionData({ actionId: currentAction });
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
// #TODO: make this a separate function and reuse across the app
let decryptedLatestKey: string;
if (latestKey) {
// assymmetrically decrypt symmetric key with local private key
decryptedLatestKey = decryptAssymmetric({
ciphertext: latestKey.latestKey.encryptedKey,
nonce: latestKey.latestKey.nonce,
publicKey: latestKey.latestKey.sender.publicKey,
privateKey: String(PRIVATE_KEY)
});
}
const decryptedSecretVersions = tempActionData.payload.secretVersions.map((encryptedSecretVersion: {
newSecretVersion?: SecretProps;
oldSecretVersion?: SecretProps;
}) => {
return {
newSecretVersion: {
key: decryptSymmetric({
ciphertext: encryptedSecretVersion.newSecretVersion!.secretKeyCiphertext,
iv: encryptedSecretVersion.newSecretVersion!.secretKeyIV,
tag: encryptedSecretVersion.newSecretVersion!.secretKeyTag,
key: decryptedLatestKey
}),
value: decryptSymmetric({
ciphertext: encryptedSecretVersion.newSecretVersion!.secretValueCiphertext,
iv: encryptedSecretVersion.newSecretVersion!.secretValueIV,
tag: encryptedSecretVersion.newSecretVersion!.secretValueTag,
key: decryptedLatestKey
})
},
oldSecretVersion: {
key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext
? decryptSymmetric({
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext,
iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV,
tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag,
key: decryptedLatestKey
}): undefined,
value: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext
? decryptSymmetric({
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext,
iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV,
tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag,
key: decryptedLatestKey
}): undefined
}
}
})
setActionData(decryptedSecretVersions);
setActionMetaData({name: tempActionData.name});
setIsLoading(false);
}
getLogData();
}, [currentAction]);
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>
{isLoading ? (
<div className="flex items-center justify-center h-full mb-8">
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="infisical loading indicator"
></Image>
</div>
) : (
<div className='h-min overflow-y-auto'>
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
<p className="font-semibold text-lg text-bunker-200">{t("activity:event." + actionMetaData?.name)}</p>
<div className='p-1' onClick={() => toggleSidebar("")}>
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
</div>
</div>
<div className='flex flex-col px-4'>
{(actionMetaData?.name == 'readSecrets'
|| actionMetaData?.name == 'addSecrets'
|| actionMetaData?.name == 'deleteSecrets') && actionData?.map((item, id) =>
<div key={id}>
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
<DashboardInputField
key={id}
onChangeHandler={() => {}}
type="value"
position={1}
value={item.newSecretVersion.value}
isDuplicate={false}
blurred={false}
/>
</div>
)}
{actionMetaData?.name == 'updateSecrets' && actionData?.map((item, id) =>
<>
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
<div className='text-bunker-100 font-mono rounded-md overflow-hidden'>
<div className='bg-red/30 px-2'>- {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.bIndex != -1 && <span key={id} className={`${character.aIndex == -1 && "bg-red-700/80"}`}>{character.line}</span>)}</div>
<div className='bg-green-500/30 px-2'>+ {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.aIndex != -1 && <span key={id} className={`${character.bIndex == -1 && "bg-green-700/80"}`}>{character.line}</span>)}</div>
</div>
</>
)}
</div>
</div>
)}
</div>
};
export default ActivitySideBar;
@@ -0,0 +1,138 @@
import React, { useState } from 'react';
import Image from 'next/image';
import { useTranslation } from "next-i18next";
import timeSince from '@app/ee/utilities/timeSince';
import {
faAngleDown,
faAngleRight,
faUpRightFromSquare
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import guidGenerator from '../../components/utilities/randomId';
interface PayloadProps {
_id: string;
name: string;
secretVersions: string[];
}
interface logData {
_id: string;
channel: string;
createdAt: string;
ipAddress: string;
user: string;
payload: PayloadProps[];
}
/**
* This is a single row of the activity table
* @param obj
* @param {logData} obj.row - data for a certain event
* @param {function} obj.toggleSidebar - open and close sidebar that displays data for a specific event
* @returns
*/
const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar: (value: string) => void; }) => {
const [payloadOpened, setPayloadOpened] = useState(false);
const { t } = useTranslation();
return (
<>
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full text-sm">
<td
onClick={() => setPayloadOpened(!payloadOpened)}
className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer"
>
<FontAwesomeIcon
icon={payloadOpened ? faAngleDown : faAngleRight}
className={`mt-2.5 ml-6 text-bunker-100 hover:bg-mineshaft-700 ${
payloadOpened && 'bg-mineshaft-500'
} p-1 duration-100 h-4 w-4 rounded-md`}
/>
</td>
<td className="py-3 border-mineshaft-700 border-t text-gray-300">
{row.payload?.map(action => String(action.secretVersions.length) + " " + t("activity:event." + action.name)).join(" and ")}
</td>
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
{row.user}
</td>
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
{row.channel}
</td>
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
{timeSince(new Date(row.createdAt))}
</td>
</tr>
{payloadOpened &&
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
<td></td>
<td>{String(t("common:timestamp"))}</td>
<td>{row.createdAt}</td>
</tr>}
{payloadOpened && row.payload?.map((action, index) => {
return action.secretVersions.length > 0 && <tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
<td></td>
<td className="">{t("activity:event." + action.name)}</td>
<td className="text-primary-300 cursor-pointer hover:text-primary duration-200" onClick={() => toggleSidebar(action._id)}>
{action.secretVersions.length + (action.secretVersions.length != 1 ? " secrets" : " secret")}
<FontAwesomeIcon icon={faUpRightFromSquare} className="ml-2 mb-0.5 font-light w-3 h-3"/>
</td>
</tr>
})}
{payloadOpened &&
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
<td></td>
<td>{String(t("activity:ip-address"))}</td>
<td>{row.ipAddress}</td>
</tr>}
</>
);
};
/**
* This is the table for activity logs (one of the tabs)
* @param {object} obj
* @param {logData} obj.data - data for user activity logs
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
* @param {boolean} obj.isLoading - whether the log data has been loaded yet or not
* @returns
*/
const ActivityTable = ({ data, toggleSidebar, isLoading }: { data: logData[], toggleSidebar: (value: string) => void; isLoading: boolean; }) => {
const { t } = useTranslation();
return (
<div className="w-full px-6 mt-8">
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative">
<div className="absolute rounded-t-md w-full h-[3rem] bg-white/5"></div>
<table className="w-full my-1">
<thead className="text-bunker-300">
<tr className='text-sm'>
<th className="text-left pl-6 pt-2.5 pb-3"></th>
<th className="text-left font-semibold pt-2.5 pb-3">{String(t("common:event")).toUpperCase()}</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:user")).toUpperCase()}</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:source")).toUpperCase()}</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:time")).toUpperCase()}</th>
<th></th>
</tr>
</thead>
<tbody>
{data?.map((row, index) => {
return <ActivityLogsRow key={index} row={row} toggleSidebar={toggleSidebar} />;
})}
</tbody>
</table>
</div>
{isLoading && <div className='w-full flex justify-center mb-8 mt-4'><Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="loading animation"
></Image></div>}
</div>
);
};
export default ActivityTable;
@@ -0,0 +1,183 @@
import { useEffect, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import getProjectSecretShanpshots from "@app/ee/api/secrets/GetProjectSercetShanpshots";
import getSecretSnapshotData from "@app/ee/api/secrets/GetSecretSnapshotData";
import timeSince from "@app/ee/utilities/timeSince";
import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from "~/components/basic/buttons/Button";
import { decryptAssymmetric, decryptSymmetric } from "~/components/utilities/cryptography/crypto";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
export interface SecretDataProps {
pos: number;
key: string;
value: string;
type: string;
id: string;
environment: string;
}
interface SideBarProps {
toggleSidebar: (value: boolean) => void;
setSnapshotData: (value: any) => void;
chosenSnapshot: string;
}
interface SnaphotProps {
_id: string;
createdAt: string;
secretVersions: string[];
}
interface EncrypetedSecretVersionListProps {
_id: string;
createdAt: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
environment: string;
type: "personal" | "shared";
}
/**
* @param {object} obj
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
* @param {function} obj.setSnapshotData - state manager for snapshot data
* @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected
* @returns the sidebar with the options for point-in-time recovery (commits)
*/
const PITRecoverySidebar = ({
toggleSidebar,
setSnapshotData,
chosenSnapshot
}: SideBarProps) => {
const { t } = useTranslation();
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [secretSnapshotsMetadata, setSecretSnapshotsMetadata] = useState<SnaphotProps[]>([]);
const [currentOffset, setCurrentOffset] = useState(0);
const currentLimit = 15;
const loadMoreSnapshots = () => {
setCurrentOffset(currentOffset + currentLimit);
}
useEffect(() => {
const getLogData = async () => {
setIsLoading(true);
const results = await getProjectSecretShanpshots({ workspaceId: String(router.query.id), limit: currentLimit, offset: currentOffset })
setSecretSnapshotsMetadata(secretSnapshotsMetadata.concat(results));
setIsLoading(false);
}
getLogData();
}, [currentOffset]);
const exploreSnapshot = async ({ snapshotId }: { snapshotId: string; }) => {
const secretSnapshotData = await getSecretSnapshotData({ secretSnapshotId: snapshotId });
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
let decryptedLatestKey: string;
if (latestKey) {
// assymmetrically decrypt symmetric key with local private key
decryptedLatestKey = decryptAssymmetric({
ciphertext: latestKey.latestKey.encryptedKey,
nonce: latestKey.latestKey.nonce,
publicKey: latestKey.latestKey.sender.publicKey,
privateKey: String(PRIVATE_KEY)
});
}
const decryptedSecretVersions = secretSnapshotData.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => {
return {
id: encryptedSecretVersion._id,
pos: pos,
type: encryptedSecretVersion.type,
environment: encryptedSecretVersion.environment,
key: decryptSymmetric({
ciphertext: encryptedSecretVersion.secretKeyCiphertext,
iv: encryptedSecretVersion.secretKeyIV,
tag: encryptedSecretVersion.secretKeyTag,
key: decryptedLatestKey
}),
value: decryptSymmetric({
ciphertext: encryptedSecretVersion.secretValueCiphertext,
iv: encryptedSecretVersion.secretValueIV,
tag: encryptedSecretVersion.secretValueTag,
key: decryptedLatestKey
})
}
})
const secretKeys = [...new Set(decryptedSecretVersions.map((secret: SecretDataProps) => secret.key))];
const result = secretKeys.map((key, index) => {
return {
id: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].id,
pos: index,
key: key,
environment: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].environment,
value: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0]?.value,
valueOverride: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'personal')[0]?.value,
}
});
setSnapshotData({ id: secretSnapshotData._id, version: secretSnapshotData.version, createdAt: secretSnapshotData.createdAt, secretVersions: result, comment: '' })
}
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>
{isLoading ? (
<div className="flex items-center justify-center h-full mb-8">
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="infisical loading indicator"
></Image>
</div>
) : (
<div className='h-min'>
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
<p className="font-semibold text-lg text-bunker-200">{t("Point-in-time Recovery")}</p>
<div className='p-1' onClick={() => toggleSidebar(false)}>
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
</div>
</div>
<div className='flex flex-col px-2 py-2 overflow-y-auto h-[92vh]'>
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) =>
<div
key={snapshot._id}
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })}
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "bg-primary text-black pointer-events-none" : "bg-mineshaft-700 hover:bg-mineshaft-500 duration-200 cursor-pointer"} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`}
>
<div className="flex flex-row items-start">
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800" : "text-bunker-200"} text-sm mr-1.5`}>{timeSince(new Date(snapshot.createdAt))}</div>
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-900" : "text-bunker-300"} text-sm `}>{" - " + snapshot.secretVersions.length + " Secrets"}</div>
</div>
<div
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800 pointer-events-none" : "text-bunker-200 hover:text-primary duration-200 cursor-pointer"} text-sm`}>
{id == 0 ? "Current Version" : chosenSnapshot == snapshot._id ? "Currently Viewing" : "Explore"}
</div>
</div>)}
<div className='flex justify-center w-full mb-14'>
<div className='items-center w-40'>
<Button text="View More" textDisabled="End of History" active={secretSnapshotsMetadata.length % 15 == 0 ? true : false} onButtonPressed={loadMoreSnapshots} size="md" color="mineshaft"/>
</div>
</div>
</div>
</div>
)}
</div>
};
export default PITRecoverySidebar;
@@ -0,0 +1,123 @@
import { useEffect, useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useTranslation } from "next-i18next";
import getSecretVersions from '@app/ee/api/secrets/GetSecretVersions';
import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { decryptAssymmetric, decryptSymmetric } from '~/components/utilities/cryptography/crypto';
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
interface DecryptedSecretVersionListProps {
createdAt: string;
value: string;
}
interface EncrypetedSecretVersionListProps {
createdAt: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
}
/**
* @param {string} secretId - the id of a secret for which are querying version history
* @returns a list of versions for a specific secret
*/
const SecretVersionList = ({ secretId }: { secretId: string; }) => {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([]);
useEffect(() => {
const getSecretVersionHistory = async () => {
setIsLoading(true);
try {
const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10});
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
let decryptedLatestKey: string;
if (latestKey) {
// assymmetrically decrypt symmetric key with local private key
decryptedLatestKey = decryptAssymmetric({
ciphertext: latestKey.latestKey.encryptedKey,
nonce: latestKey.latestKey.nonce,
publicKey: latestKey.latestKey.sender.publicKey,
privateKey: String(PRIVATE_KEY)
});
}
const decryptedSecretVersions = encryptedSecretVersions?.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps) => {
return {
createdAt: encryptedSecretVersion.createdAt,
value: decryptSymmetric({
ciphertext: encryptedSecretVersion.secretValueCiphertext,
iv: encryptedSecretVersion.secretValueIV,
tag: encryptedSecretVersion.secretValueTag,
key: decryptedLatestKey
})
}
})
setSecretVersions(decryptedSecretVersions);
setIsLoading(false);
} catch (error) {
console.log(error)
}
};
getSecretVersionHistory();
}, [secretId]);
return <div className='w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none'>
<p className=''>{t("dashboard:sidebar.version-history")}</p>
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full'>
{isLoading ? (
<div className="flex items-center justify-center h-full">
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="infisical loading indicator"
></Image>
</div>
) : (
<div className='h-48 overflow-y-auto overflow-x-none'>
{secretVersions
? secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
.map((version: DecryptedSecretVersionListProps, index: number) =>
<div key={index} className='flex flex-row'>
<div className='pr-1 flex flex-col items-center'>
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
<div className='w-0 h-full border-l mt-1'></div>
</div>
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
<div className='pr-2 pt-1'>
{(new Date(version.createdAt)).toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
</div>
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
</div>
</div>
)
: (
<div className='w-full h-full flex items-center justify-center text-bunker-400'>No version history yet.</div>
)
}
</div>
)}
</div>
</div>
};
export default SecretVersionList;
@@ -0,0 +1,346 @@
/**
*
* @param textOld - old secret
* @param textNew - new (updated) secret
* @param diffPlusFlag - a flag for whether we want to detect moving segments
* - doesn't work in some examples (e.g., when we have a full reverse ordering of the text)
* @returns
*/
function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boolean) {
/**
* findUnique finds all unique values in arr[lo..hi], inclusive. This
* function is used in preparation for determining the longest common
* subsequence. Specifically, it first reduces the array range in question
* to unique values.
* @param chars - an array of characters
* @param lo
* @param hi
* @returns - an ordered Map, with the arr[i] value as the Map key and the
* array index i as the Map value.
*/
function findUnique(chars: string[], lo: number, hi: number) {
const characterMap = new Map();
for (let i=lo; i<=hi; i++) {
const character = chars[i];
if (characterMap.has(character)) {
characterMap.get(character).count++;
characterMap.get(character).index = i;
} else {
characterMap.set(character, { count: 1, index: i });
}
}
characterMap.forEach((val, key, map) => {
if (val.count !== 1) {
map.delete(key);
} else {
map.set(key, val.index);
}
});
return characterMap;
}
/**
* @param aArray
* @param aLo
* @param aHi
* @param bArray
* @param bLo
* @param bHi
* @returns an ordered Map, with the Map key as the common line between aArray
* and bArray, with the Map value as an object containing the array indexes of
* the matching unique lines.
*
*/
function uniqueCommon(aArray: string[], aLo: number, aHi: number, bArray: string[], bLo: number, bHi: number) {
const ma = findUnique(aArray, aLo, aHi);
const mb = findUnique(bArray, bLo, bHi);
ma.forEach((val, key, map) => {
if (mb.has(key)) {
map.set(key, {
indexA: val,
indexB: mb.get(key)
});
} else {
map.delete(key);
}
});
return ma;
}
/**
* longestCommonSubsequence takes an ordered Map from the function uniqueCommon
* and determines the Longest Common Subsequence (LCS).
* @param abMap
* @returns an ordered array of objects containing the array indexes of the
* matching lines for a LCS.
*/
function longestCommonSubsequence(abMap: Map<number, { indexA: number, indexB: number, prev?: number }>) {
const ja: any = [];
// First, walk the list creating the jagged array.
abMap.forEach((val, key, map) => {
let i = 0;
while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) {
i++;
}
if (!ja[i]) {
ja[i] = [];
}
if (0 < i) {
val.prev = ja[i-1][ja[i - 1].length - 1];
}
ja[i].push(val);
});
// Now, pull out the longest common subsequence.
let lcs: any[] = [];
if (0 < ja.length) {
const n = ja.length - 1;
lcs = [ja[n][ja[n].length - 1]];
while (lcs[lcs.length - 1].prev) {
lcs.push(lcs[lcs.length - 1].prev);
}
}
return lcs.reverse();
}
// "result" is the array used to accumulate the textOld that are deleted, the
// lines that are shared between textOld and textNew, and the textNew that were
// inserted.
const result: any[] = [];
let deleted = 0;
let inserted = 0;
// aMove and bMove will contain the lines that don't match, and will be returned
// for possible searching of lines that moved.
const aMove: any[] = [];
const aMoveIndex: any[] = [];
const bMove: any[] = [];
const bMoveIndex: any[] = [];
/**
* addToResult simply pushes the latest value onto the "result" array. This
* array captures the diff of the line, aIndex, and bIndex from the textOld
* and textNew array.
* @param aIndex
* @param bIndex
*/
function addToResult(aIndex: number, bIndex: number) {
if (bIndex < 0) {
aMove.push(textOld[aIndex]);
aMoveIndex.push(result.length);
deleted++;
} else if (aIndex < 0) {
bMove.push(textNew[bIndex]);
bMoveIndex.push(result.length);
inserted++;
}
result.push({
line: 0 <= aIndex ? textOld[aIndex] : textNew[bIndex],
aIndex: aIndex,
bIndex: bIndex,
});
}
/**
* addSubMatch handles the lines between a pair of entries in the LCS. Thus,
* this function might recursively call recurseLCS to further match the lines
* between textOld and textNew.
* @param aLo
* @param aHi
* @param bLo
* @param bHi
*/
function addSubMatch(aLo: number, aHi: number, bLo: number, bHi: number) {
// Match any lines at the beginning of textOld and textNew.
while (aLo <= aHi && bLo <= bHi && textOld[aLo] === textNew[bLo]) {
addToResult(aLo++, bLo++);
}
// Match any lines at the end of textOld and textNew, but don't place them
// in the "result" array just yet, as the lines between these matches at
// the beginning and the end need to be analyzed first.
const aHiTemp = aHi;
while (aLo <= aHi && bLo <= bHi && textOld[aHi] === textNew[bHi]) {
aHi--;
bHi--;
}
// Now, check to determine with the remaining lines in the subsequence
// whether there are any unique common lines between textOld and textNew.
//
// If not, add the subsequence to the result (all textOld having been
// deleted, and all textNew having been inserted).
//
// If there are unique common lines between textOld and textNew, then let's
// recursively perform the patience diff on the subsequence.
const uniqueCommonMap = uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi);
if (uniqueCommonMap.size === 0) {
while (aLo <= aHi) {
addToResult(aLo++, -1);
}
while (bLo <= bHi) {
addToResult(-1, bLo++);
}
} else {
recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap);
}
// Finally, let's add the matches at the end to the result.
while (aHi < aHiTemp) {
addToResult(++aHi, ++bHi);
}
}
/**
* recurseLCS finds the longest common subsequence (LCS) between the arrays
* textOld[aLo..aHi] and textNew[bLo..bHi] inclusive. Then for each subsequence
* recursively performs another LCS search (via addSubMatch), until there are
* none found, at which point the subsequence is dumped to the result.
* @param aLo
* @param aHi
* @param bLo
* @param bHi
* @param uniqueCommonMap
*/
function recurseLCS(aLo: number, aHi: number, bLo: number, bHi: number, uniqueCommonMap?: any) {
const x = longestCommonSubsequence(uniqueCommonMap || uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi));
if (x.length === 0) {
addSubMatch(aLo, aHi, bLo, bHi);
} else {
if (aLo < x[0].indexA || bLo < x[0].indexB) {
addSubMatch(aLo, x[0].indexA - 1, bLo, x[0].indexB - 1);
}
let i;
for (i = 0; i < x.length - 1; i++) {
addSubMatch(x[i].indexA, x[i+1].indexA - 1, x[i].indexB, x[i+1].indexB - 1);
}
if (x[i].indexA <= aHi || x[i].indexB <= bHi) {
addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi);
}
}
}
recurseLCS(0, textOld.length - 1, 0, textNew.length - 1);
if (diffPlusFlag) {
return {
lines: result,
lineCountDeleted: deleted,
lineCountInserted: inserted,
lineCountMoved: 0,
aMove: aMove,
aMoveIndex: aMoveIndex,
bMove: bMove,
bMoveIndex: bMoveIndex,
};
}
return {
lines: result,
lineCountDeleted: deleted,
lineCountInserted: inserted,
lineCountMoved: 0,
};
}
/**
* use: patienceDiffPlus( textOld[], textNew[] )
*
* where:
* textOld[] contains the original text lines.
* textNew[] contains the new text lines.
*
* returns an object with the following properties:
* lines[] with properties of:
* line containing the line of text from textOld or textNew.
* aIndex referencing the index in aLine[].
* bIndex referencing the index in textNew[].
* (Note: The line is text from either textOld or textNew, with aIndex and bIndex
* referencing the original index. If aIndex === -1 then the line is new from textNew,
* and if bIndex === -1 then the line is old from textOld.)
* moved is true if the line was moved from elsewhere in textOld[] or textNew[].
* lineCountDeleted is the number of lines from textOld[] not appearing in textNew[].
* lineCountInserted is the number of lines from textNew[] not appearing in textOld[].
* lineCountMoved is the number of lines that moved.
*/
function patienceDiffPlus(textOld: string[], textNew: string[]) {
const difference = patienceDiff(textOld, textNew, true);
let aMoveNext = difference.aMove;
let aMoveIndexNext = difference.aMoveIndex;
let bMoveNext = difference.bMove;
let bMoveIndexNext = difference.bMoveIndex;
delete difference.aMove;
delete difference.aMoveIndex;
delete difference.bMove;
delete difference.bMoveIndex;
let lastLineCountMoved;
do {
const aMove = aMoveNext;
const aMoveIndex = aMoveIndexNext;
const bMove = bMoveNext;
const bMoveIndex = bMoveIndexNext;
aMoveNext = [];
aMoveIndexNext = [];
bMoveNext = [];
bMoveIndexNext = [];
const subDiff = patienceDiff(aMove!, bMove!);
lastLineCountMoved = difference.lineCountMoved;
subDiff.lines.forEach((v, i) => {
if (0 <= v.aIndex && 0 <= v.bIndex) {
difference.lines[aMoveIndex![v.aIndex]].moved = true;
difference.lines[bMoveIndex![v.bIndex]].aIndex = aMoveIndex![v.aIndex];
difference.lines[bMoveIndex![v.bIndex]].moved = true;
difference.lineCountInserted--;
difference.lineCountDeleted--;
difference.lineCountMoved++;
} else if (v.bIndex < 0) {
aMoveNext!.push(aMove![v.aIndex]);
aMoveIndexNext!.push(aMoveIndex![v.aIndex]);
} else {
bMoveNext!.push(bMove![v.bIndex]);
bMoveIndexNext!.push(bMoveIndex![v.bIndex]);
}
});
} while (0 < difference.lineCountMoved - lastLineCountMoved);
return difference;
}
export default patienceDiff;
+35
View File
@@ -0,0 +1,35 @@
/**
* Time since a certain date
* @param {Date} date - the timestamp got which we want to understand how long ago it happened
* @returns {String} text - how much time has passed since a certain timestamp
*/
function timeSince(date: Date) {
const seconds = Math.floor(
((new Date() as any) - (date as any)) / 1000
) as number;
let interval = seconds / 31536000;
if (interval > 1) {
return Math.floor(interval) + ' years ago';
}
interval = seconds / 2592000;
if (interval > 1) {
return Math.floor(interval) + ' months ago';
}
interval = seconds / 86400;
if (interval > 1) {
return Math.floor(interval) + ' days ago';
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + ' hours ago';
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + ' minutes ago';
}
return Math.floor(seconds) + ' seconds ago';
}
export default timeSince;