mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
Finished activity logs V1
This commit is contained in:
@ -6,6 +6,7 @@ import {
|
||||
faEye,
|
||||
faPlus,
|
||||
faShuffle,
|
||||
faTrash,
|
||||
faX
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
@ -28,6 +29,10 @@ const eventOptions = [
|
||||
{
|
||||
name: 'updateSecrets',
|
||||
icon: faShuffle
|
||||
},
|
||||
{
|
||||
name: 'deleteSecrets',
|
||||
icon: faTrash
|
||||
}
|
||||
];
|
||||
|
||||
|
32
frontend/ee/api/secrets/GetActionData.ts
Normal file
32
frontend/ee/api/secrets/GetActionData.ts
Normal file
@ -0,0 +1,32 @@
|
||||
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) => {
|
||||
console.log(188, 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;
|
@ -1,79 +1,184 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import getActionData from "ee/api/secrets/GetActionData";
|
||||
import patienceDiff from 'ee/utilities/findTextDifferences';
|
||||
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
import DashboardInputField from '../../components/dashboard/DashboardInputField';
|
||||
|
||||
|
||||
const secretChanges = [{
|
||||
"oldSecret": "secret1",
|
||||
"newSecret": "ecret2"
|
||||
}, {
|
||||
"oldSecret": "secret1",
|
||||
"newSecret": "sercet2"
|
||||
}, {
|
||||
"oldSecret": "localhosta:8080",
|
||||
"newSecret": "aaaalocalhoats:3000"
|
||||
}]
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} = require('../../components/utilities/cryptography/crypto');
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: string[]) => void;
|
||||
sidebarData: string[];
|
||||
currentEvent: string;
|
||||
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.secretIds - data of payload
|
||||
* @param {string} obj.currentEvent - the event name for which a sidebar is being displayed
|
||||
* @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,
|
||||
sidebarData,
|
||||
currentEvent
|
||||
currentAction
|
||||
}: SideBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [actionData, setActionData] = useState<DecryptedSecretProps[]>();
|
||||
const [actionMetaData, setActionMetaData] = useState<ActionProps>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between'>
|
||||
<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." + currentEvent)}</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar([])}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
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-50 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 className='flex flex-col px-4'>
|
||||
{currentEvent == 'readSecrets' && sidebarData.map((item, id) =>
|
||||
<>
|
||||
<div className='text-sm text-bunker-200 mt-4 pl-1'>Key {id}</div>
|
||||
<DashboardInputField
|
||||
key={id}
|
||||
onChangeHandler={() => {}}
|
||||
type="varName"
|
||||
position={1}
|
||||
value={"a" + item}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{currentEvent == 'updateSecrets' && sidebarData.map((item, id) =>
|
||||
secretChanges.map(secretChange =>
|
||||
<>
|
||||
<div className='text-sm text-bunker-200 mt-4 pl-1'>Secret Name {id}</div>
|
||||
<div className='text-bunker-100 font-mono rounded-md overflow-hidden'>
|
||||
<div className='bg-red/30 px-2'>- {patienceDiff(secretChange.oldSecret.split(''), secretChange.newSecret.split(''), false).lines.map((character, id) => character.aIndex != -1 && <span key={id} className={`${character.bIndex == -1 && "bg-red-700/80"}`}>{character.line}</span>)}</div>
|
||||
<div className='bg-green-500/30 px-2'>+ {patienceDiff(secretChange.oldSecret.split(''), secretChange.newSecret.split('')).lines.map((character, id) => character.bIndex != -1 && <span key={id} className={`${character.aIndex == -1 && "bg-green-700/80"}`}>{character.line}</span>)}</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</div>
|
||||
};
|
||||
|
||||
|
@ -4,8 +4,7 @@ import { useTranslation } from "next-i18next";
|
||||
import {
|
||||
faAngleDown,
|
||||
faAngleRight,
|
||||
faUpRightFromSquare,
|
||||
faX
|
||||
faUpRightFromSquare
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import timeSince from 'ee/utilities/timeSince';
|
||||
@ -14,6 +13,7 @@ import guidGenerator from '../../components/utilities/randomId';
|
||||
|
||||
|
||||
interface PayloadProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
@ -29,25 +29,26 @@ interface logData {
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* This is a single row of the activity table
|
||||
* @param obj
|
||||
* @param {function} obj.setCurrentEvent - specify the name of the event for which the sidebar is being opened
|
||||
* @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, setCurrentEvent }: { row: logData, toggleSidebar: (value: string[]) => void; setCurrentEvent: (value: string) => void; }) => {
|
||||
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">
|
||||
<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-3 ml-6 text-bunker-100 hover:bg-mineshaft-700 ${
|
||||
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`}
|
||||
/>
|
||||
@ -66,26 +67,23 @@ const ActivityLogsRow = ({ row, toggleSidebar, setCurrentEvent }: { row: logData
|
||||
</td>
|
||||
</tr>
|
||||
{payloadOpened &&
|
||||
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t'>
|
||||
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
|
||||
<td></td>
|
||||
<td>Timestamp</td>
|
||||
<td>{row.createdAt}</td>
|
||||
</tr>}
|
||||
{payloadOpened &&
|
||||
row.payload?.map((action, index) =>
|
||||
<tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t">
|
||||
<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.secretVersions);
|
||||
setCurrentEvent(action.name);
|
||||
}}>
|
||||
<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'>
|
||||
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
|
||||
<td></td>
|
||||
<td>IP Address</td>
|
||||
<td>{row.ipAddress}</td>
|
||||
@ -99,28 +97,27 @@ const ActivityLogsRow = ({ row, toggleSidebar, setCurrentEvent }: { row: logData
|
||||
* @param {object} obj
|
||||
* @param {logData} obj.data - data for user activity logs
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @param {function} obj.setCurrentEvent - specify the name of the event for which the sidebar is being opened
|
||||
* @returns
|
||||
*/
|
||||
const ActivityTable = ({ data, toggleSidebar, setCurrentEvent }: { data: logData[], toggleSidebar: (value: string[]) => void; setCurrentEvent: (value: string) => void; }) => {
|
||||
const ActivityTable = ({ data, toggleSidebar }: { data: logData[], toggleSidebar: (value: string) => void; }) => {
|
||||
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-[3.15rem] bg-white/5"></div>
|
||||
<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>
|
||||
<tr className='text-sm'>
|
||||
<th className="text-left pl-6 pt-2.5 pb-3"></th>
|
||||
<th className="text-left pt-2.5 pb-3">Event</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-3">User</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-3">Source</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-3">Time</th>
|
||||
<th className="text-left font-semibold pt-2.5 pb-3">EVENT</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">USER</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">SOURCE</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">TIME</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.map((row, index) => {
|
||||
return <ActivityLogsRow key={index} row={row} toggleSidebar={toggleSidebar} setCurrentEvent={setCurrentEvent} />;
|
||||
return <ActivityLogsRow key={index} row={row} toggleSidebar={toggleSidebar} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -21,6 +21,7 @@ interface logData {
|
||||
email: string;
|
||||
};
|
||||
actions: {
|
||||
_id: string;
|
||||
name: string;
|
||||
payload: {
|
||||
secretVersions: string[];
|
||||
@ -29,6 +30,7 @@ interface logData {
|
||||
}
|
||||
|
||||
interface PayloadProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
@ -51,8 +53,7 @@ export default function Activity() {
|
||||
const [logsData, setLogsData] = useState<logDataPoint[]>([]);
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
const currentLimit = 10;
|
||||
const [sidebarData, toggleSidebar] = useState<string[]>([])
|
||||
const [currentEvent, setCurrentEvent] = useState("");
|
||||
const [currentSidebarAction, toggleSidebar] = useState<string>()
|
||||
const { t } = useTranslation();
|
||||
|
||||
// this use effect updates the data in case of a new filter being added
|
||||
@ -69,6 +70,7 @@ export default function Activity() {
|
||||
user: log.user.email,
|
||||
payload: log.actions.map(action => {
|
||||
return {
|
||||
_id: action._id,
|
||||
name: action.name,
|
||||
secretVersions: action.payload.secretVersions
|
||||
}
|
||||
@ -92,6 +94,7 @@ export default function Activity() {
|
||||
user: log.user.email,
|
||||
payload: log.actions.map(action => {
|
||||
return {
|
||||
_id: action._id,
|
||||
name: action.name,
|
||||
secretVersions: action.payload.secretVersions
|
||||
}
|
||||
@ -109,13 +112,13 @@ export default function Activity() {
|
||||
return (
|
||||
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll h-screen">
|
||||
<NavHeader pageName="Project Activity" isProjectRelated={true} />
|
||||
{sidebarData.length > 0 && <ActivitySideBar sidebarData={sidebarData} toggleSidebar={toggleSidebar} currentEvent={currentEvent} />}
|
||||
{currentSidebarAction && <ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />}
|
||||
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
|
||||
<div className="flex flex-row justify-start items-center text-3xl">
|
||||
<p className="font-semibold mr-4 text-bunker-100">Activity Logs</p>
|
||||
</div>
|
||||
<p className="mr-4 text-base text-gray-400">
|
||||
Event history limited to the last 12 months.
|
||||
Event history for this Infisical project.
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-6 h-8 mt-2">
|
||||
@ -127,7 +130,6 @@ export default function Activity() {
|
||||
<ActivityTable
|
||||
data={logsData}
|
||||
toggleSidebar={toggleSidebar}
|
||||
setCurrentEvent={setCurrentEvent}
|
||||
/>
|
||||
<div className='flex justify-center w-full mb-6'>
|
||||
<div className='items-center w-60'>
|
||||
|
Reference in New Issue
Block a user