Fast forward

This commit is contained in:
Tuan Dang
2023-08-11 12:46:30 +07:00
355 changed files with 6255 additions and 7991 deletions
@@ -149,7 +149,7 @@ export const DashboardPage = () => {
const [snapshotId, setSnaphotId] = useState<string | null>(null);
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
const deletedSecretIds = useRef<string[]>([]);
const deletedSecretIds = useRef<{ id: string; secretName: string; }[]>([]);
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false });
const folderId = router.query.folderId as string;
@@ -498,9 +498,15 @@ export const DashboardPage = () => {
// record all deleted ids
// This will make final deletion easier
const onSecretDelete = useCallback((index: number, id?: string, overrideId?: string) => {
if (id) deletedSecretIds.current.push(id);
if (overrideId) deletedSecretIds.current.push(overrideId);
const onSecretDelete = useCallback((index: number, secretName: string, id?: string, overrideId?: string) => {
if (id) deletedSecretIds.current.push({
id,
secretName
});
if (overrideId) deletedSecretIds.current.push({
id: overrideId,
secretName
});
remove(index);
// just the case if this is called from drawer
handlePopUpClose("secretDetails");
@@ -944,6 +950,7 @@ export const DashboardPage = () => {
secrets={secrets?.secrets}
importedSecrets={importedSecrets}
items={items}
searchTerm={searchFilter}
/>
<FolderSection
onFolderOpen={handleFolderOpen}
@@ -182,15 +182,18 @@ const deepCompareSecrets = (lhs: DecryptedSecret, rhs: any) =>
JSON.stringify(lhs.tags) === JSON.stringify(rhs.tags);
export const transformSecretsToBatchSecretReq = (
deletedSecretIds: string[],
deletedSecretIds: { id: string; secretName: string; }[],
latestFileKey: any,
secrets: FormData["secrets"],
intialValues: DecryptedSecret[] = []
) => {
// deleted secrets
const secretsToBeDeleted: BatchSecretDTO["requests"] = deletedSecretIds.map((id) => ({
const secretsToBeDeleted: BatchSecretDTO["requests"] = deletedSecretIds.map(({ id, secretName }) => ({
method: "DELETE",
secret: { _id: id }
secret: {
_id: id,
secretName
}
}));
const secretsToBeUpdated: BatchSecretDTO["requests"] = [];
@@ -261,7 +264,7 @@ export const transformSecretsToBatchSecretReq = (
if (idOverride) {
// if action is deleted meaning override has been removed but id is kept to collect at this point
if (overrideAction === SecretActionType.Deleted) {
secretsToBeDeleted.push({ method: "DELETE", secret: { _id: idOverride } });
secretsToBeDeleted.push({ method: "DELETE", secret: { _id: idOverride, secretName: key } });
} else {
// if not deleted action then as id is there its an updated
const initialSecretValue = intialValues?.find(({ _id: secId }) => secId === _id)!;
@@ -27,7 +27,7 @@ type Props = {
onEnvCompare: (secretKey: string) => void;
secretVersion?: Array<{ id: string; createdAt: string; value: string }>;
// to record the ids of deleted ones
onSecretDelete: (index: number, id?: string, overrideId?: string) => void;
onSecretDelete: (index: number, secretName: string, id?: string, overrideId?: string) => void;
onSave: () => void;
};
@@ -93,7 +93,8 @@ export const SecretDetailDrawer = ({
isDisabled={isReadOnly}
onClick={() => {
const secret = getValues(`secrets.${index}`);
onSecretDelete(index, secret._id, secret.idOverride);
onSecretDelete(index, secret.key, secret._id, secret.idOverride);
}}
>
Delete
@@ -115,7 +115,9 @@ export const SecretDropzone = ({
workspaceId,
env: selectedEnvSlug,
secretPath: debouncedSecretPath,
isPaused: !(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedSecretPath)),
isPaused:
!(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedSecretPath)) &&
!popUp.importSecEnv.isOpen,
decryptFileKey
});
@@ -18,6 +18,7 @@ type Props = {
importedEnv: string;
importedSecPath: string;
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
searchTerm: string;
};
// to show the environment and folder icon
@@ -37,7 +38,8 @@ export const SecretImportItem = ({
importedEnv,
importedSecPath,
onDelete,
importedSecrets = []
importedSecrets = [],
searchTerm = ""
}: Props) => {
const [isExpanded, setIsExpanded] = useToggle();
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
@@ -46,6 +48,17 @@ export const SecretImportItem = ({
const { currentWorkspace } = useWorkspace();
const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv);
useEffect(() => {
const filteredSecrets = importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase()))
if (filteredSecrets.length > 0 && searchTerm) {
setIsExpanded.on();
} else {
setIsExpanded.off();
}
}, [searchTerm]);
useEffect(() => {
if (isDragging) {
setIsExpanded.off();
@@ -65,8 +78,10 @@ export const SecretImportItem = ({
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
onClick={() => setIsExpanded.toggle()}
>
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
<FontAwesomeIcon icon={faFileImport} className="text-green-700" />
<td className={`ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4 ${isExpanded && "border-t-2 border-mineshaft-500"}`}>
<Tooltip content="Secret Import" className="capitalize">
<FontAwesomeIcon icon={faFileImport} className="text-green-700" />
</Tooltip>
</td>
<td
colSpan={2}
@@ -112,9 +127,8 @@ export const SecretImportItem = ({
</tr>
<tr>
{isExpanded && !isDragging && (
<td colSpan={3}>
<div className="rounded-md bg-bunker-700 p-4 pb-6">
<div className="mb-2 text-lg font-medium">Secrets Imported</div>
<td colSpan={3} className={`bg-bunker-800 ${isExpanded && "border-b-2 border-mineshaft-500"}`}>
<div className="rounded-md bg-bunker-700 p-1">
<TableContainer>
<table className="secret-table">
<thead>
@@ -132,7 +146,7 @@ export const SecretImportItem = ({
</td>
</tr>
)}
{importedSecrets.map(({ key, value, overriden }, index) => (
{importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase())).map(({ key, value, overriden }, index) => (
<tr key={`${importedEnv}-${importedSecPath}-${key}-${index + 1}`}>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
{key}
@@ -61,10 +61,11 @@ type Props = {
importedSecrets?: TImportedSecrets;
onSecretImportDelete: (env: string, secPath: string) => void;
items: { id: string; environment: string; secretPath: string }[];
searchTerm: string;
};
export const SecretImportSection = memo(
({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [] }: Props) => {
({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [], searchTerm = "" }: Props) => {
const { currentWorkspace } = useWorkspace();
const environments = currentWorkspace?.environments || [];
@@ -83,6 +84,7 @@ export const SecretImportSection = memo(
)}
onDelete={onSecretImportDelete}
importedSecPath={impSecPath}
searchTerm={searchTerm}
/>
))}
</SortableContext>
@@ -67,7 +67,7 @@ type Props = {
isSecretValueHidden: boolean;
searchTerm: string;
// to record the ids of deleted ones
onSecretDelete: (index: number, id?: string, overrideId?: string) => void;
onSecretDelete: (index: number, secretName: string, id?: string, overrideId?: string) => void;
// sidebar control props
onRowExpand: (secId: string | undefined, index: number) => void;
// tag props
@@ -219,7 +219,7 @@ export const SecretInputRow = memo(
}
return (
<tr className="group flex flex-row" key={index}>
<tr className="group flex flex-row hover:bg-mineshaft-700" key={index}>
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
</td>
@@ -495,7 +495,9 @@ export const SecretInputRow = memo(
colorSchema="danger"
ariaLabel="delete"
isDisabled={isReadOnly || isRollbackMode}
onClick={() => onSecretDelete(index, secId, idOverride)}
onClick={() => {
onSecretDelete(index, secKey, secId, idOverride);
}}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
+2 -2
View File
@@ -2,8 +2,8 @@ import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import axios from "axios"
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchUserDetails } from "@app/hooks/api/users/queries";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
import {
@@ -24,7 +24,7 @@ export const Login = () => {
// TODO(akhilmhdh): workspace will be controlled by a workspace context
const redirectToDashboard = async () => {
try {
const userOrgs = await getOrganizations();
const userOrgs = await fetchOrganizations();
// userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
const userOrg = userOrgs[0] && userOrgs[0]._id;
@@ -12,8 +12,8 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { Button, Input } from "@app/components/v2";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import getOrganizations from "@app/pages/api/organization/getOrgs";
type Props = {
setStep: (step: number) => void;
@@ -90,7 +90,7 @@ export const InitialStep = ({
setIsLoading(false);
return;
}
const userOrgs = await getOrganizations();
const userOrgs = await fetchOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
// case: login does not require MFA step
@@ -10,7 +10,7 @@ import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa"
import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
import { Button } from "@app/components/v2";
import { useSendMfaToken } from "@app/hooks/api/auth";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
// The style for the verification code input
const props = {
@@ -110,7 +110,7 @@ export const MFAStep = ({
if (isLoginSuccessful) {
setIsLoading(false);
const userOrgs = await getOrganizations();
const userOrgs = await fetchOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
// case: login does not require MFA step
@@ -8,7 +8,7 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { Button, Input } from "@app/components/v2";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
type Props = {
providerAuthToken: string;
@@ -83,14 +83,14 @@ export const PasswordStep = ({
}
// case: login does not require MFA step
const userOrgs = await getOrganizations();
const userOrgs = await fetchOrganizations();
const userOrg = userOrgs[0]._id;
setIsLoading(false);
createNotification({
text: "Successfully logged in",
type: "success"
});
router.push(`/org/${userOrg?._id}/overview`);
router.push(`/org/${userOrg}/overview`);
}
}
} catch (err) {
@@ -0,0 +1,17 @@
import {
LogsSection
} from "./components";
export const AuditLogsPage = () => {
return (
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<div className="max-w-7xl px-6 w-full">
<div className="my-6">
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
<div />
</div>
<LogsSection />
</div>
</div>
);
}
@@ -0,0 +1,214 @@
import { useState } from "react";
import { Control, Controller, UseFormReset } from "react-hook-form";
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
DatePicker,
FormControl,
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useGetAuditLogActorFilterOpts } from "@app/hooks/api";
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { Actor } from "@app/hooks/api/auditLogs/types";
import { AuditLogFilterFormData } from "./types";
const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value }));
const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({ label, value }));
type Props = {
control: Control<AuditLogFilterFormData>;
reset: UseFormReset<AuditLogFilterFormData>;
}
export const LogsFilter = ({
control,
reset
}: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?._id ?? "");
const renderActorSelectItem = (actor: Actor) => {
switch (actor.type) {
case ActorType.USER:
return (
<SelectItem value={`${actor.type}-${actor.metadata.userId}`} key={`user-actor-filter-${actor.metadata.userId}`}>
{actor.metadata.email}
</SelectItem>
);
case ActorType.SERVICE:
return (
<SelectItem value={`${actor.type}-${actor.metadata.serviceId}`} key={`service-actor-filter-${actor.metadata.serviceId}`}>
{actor.metadata.name}
</SelectItem>
);
default:
return (
<SelectItem value="actor-none" key="actor-none">
N/A
</SelectItem>
);
}
}
return (
<div className="flex justify-between items-center">
<div className="flex items-center">
<Controller
control={control}
name="eventType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Event"
errorText={error?.message}
isError={Boolean(error)}
className="w-40 mr-4"
>
<Select
placeholder="Select"
// {...(field.value ? { value: field.value } : { placeholder: "Select" })}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full bg-mineshaft-700 border border-mineshaft-500 text-mineshaft-100"
>
{eventTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{!isLoading && data && data.length > 0 && (
<Controller
control={control}
name="actor"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Actor"
errorText={error?.message}
isError={Boolean(error)}
className="w-40 mr-4"
>
<Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full bg-mineshaft-700 border border-mineshaft-500 text-mineshaft-100"
>
{data.map((actor) => renderActorSelectItem(actor))}
</Select>
</FormControl>
)}
/>
)}
<Controller
control={control}
name="userAgentType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Source"
errorText={error?.message}
isError={Boolean(error)}
className="w-40 mr-4"
>
<Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full bg-mineshaft-700 border border-mineshaft-500 text-mineshaft-100"
>
{userAgentTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
name="startDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="Start date"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4"
>
<DatePicker
value={field.value || undefined}
onChange={date => {
onChange(date);
setIsStartDatePickerOpen(false);
}}
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
<Controller
name="endDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="End date"
errorText={error?.message}
isError={Boolean(error)}
>
<DatePicker
value={field.value || undefined}
onChange={date => {
onChange(date);
setIsEndDatePickerOpen(false);
}}
popUpProps={{
open: isEndDatePickerOpen,
onOpenChange: setIsEndDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
</div>
<div>
<Button
isLoading={false}
colorSchema="primary"
variant="outline_bg"
type="submit"
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-2" />}
onClick={() => reset({
eventType: undefined,
actor: undefined,
userAgentType: undefined,
startDate: undefined,
endDate: undefined
})}
>
Clear filters
</Button>
</div>
</div>
);
}
@@ -0,0 +1,59 @@
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { LogsFilter } from "./LogsFilter";
import { LogsTable } from "./LogsTable";
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
export const LogsSection = () => {
const {
control,
reset,
watch,
setValue,
} = useForm<AuditLogFilterFormData>({
resolver: yupResolver(auditLogFilterFormSchema),
defaultValues: {
page: 1,
perPage: 10
}
});
const eventType = watch("eventType") as EventType | undefined;
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
const actor = watch("actor");
const startDate = watch("startDate");
const endDate = watch("endDate");
const page = watch("page") as number;
const perPage = watch("perPage") as number;
return (
<div
// className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
{/* <div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">
Audit Logs
</h2>
</div> */}
<LogsFilter
control={control}
reset={reset}
/>
<LogsTable
eventType={eventType}
userAgentType={userAgentType}
actor={actor}
startDate={startDate}
endDate={endDate}
page={page}
perPage={perPage}
setValue={setValue}
/>
</div>
);
}
@@ -0,0 +1,98 @@
import { faFile } from "@fortawesome/free-solid-svg-icons";
import {
EmptyState,
Pagination,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useGetAuditLogs } from "@app/hooks/api";
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { LogsTableRow } from "./LogsTableRow";
import { SetValueType } from "./types";
type Props = {
eventType?: EventType;
userAgentType?: UserAgentType;
actor?: string;
startDate?: Date;
endDate?: Date;
page: number;
perPage: number;
setValue: SetValueType;
}
export const LogsTable = ({
eventType,
userAgentType,
actor,
startDate,
endDate,
page,
perPage,
setValue
}: Props) => {
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetAuditLogs(currentWorkspace?._id ?? "", {
eventType,
userAgentType,
actor,
startDate,
endDate,
offset: (page - 1) * perPage,
limit: perPage
});
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Timestamp</Th>
<Th>Event</Th>
<Th>Actor</Th>
<Th>Source</Th>
<Th>Metadata</Th>
</Tr>
</THead>
<TBody>
{!isLoading && data?.auditLogs && data?.auditLogs.map((auditLog) => (
<LogsTableRow
auditLog={auditLog}
key={`audit-log-${auditLog._id}`}
/>
))}
{isLoading && <TableSkeleton innerKey="logs-table" columns={5} key="logs" />}
{!isLoading && data?.auditLogs && data?.auditLogs.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No audit logs on file"
icon={faFile}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
{!isLoading && data?.totalCount !== undefined && (
<Pagination
count={data?.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setValue("page", newPage)}
onChangePerPage={(newPerPage) => setValue("perPage", newPerPage)}
/>
)}
</TableContainer>
);
}
// TODO: retrieve count
@@ -0,0 +1,340 @@
import {
Td,
Tr
} from "@app/components/v2";
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
import { Actor, AuditLog, Event } from "@app/hooks/api/auditLogs/types";
type Props = {
auditLog: AuditLog
}
export const LogsTableRow = ({
auditLog
}: Props) => {
const renderActor = (actor: Actor) => {
switch (actor.type) {
case ActorType.USER:
return (
<Td>
<p>{actor.metadata.email}</p>
<p>User</p>
</Td>
);
case ActorType.SERVICE:
return (
<Td>
<p>{`${actor.metadata.name}`}</p>
<p>Service token</p>
</Td>
);
default:
return (
<Td />
);
}
}
const renderMetadata = (event: Event) => {
switch (event.type) {
case EventType.GET_SECRETS:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`# Secrets: ${event.metadata.numberOfSecrets}`}</p>
</Td>
);
case EventType.GET_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.CREATE_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.UPDATE_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.DELETE_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.AUTHORIZE_INTEGRATION:
return (
<Td>
<p>{`Integration: ${event.metadata.integration}`}</p>
</Td>
);
case EventType.UNAUTHORIZE_INTEGRATION:
return (
<Td>
<p>{`Integration: ${event.metadata.integration}`}</p>
</Td>
);
case EventType.CREATE_INTEGRATION:
return (
<Td>
<p>{`Integration: ${event.metadata.integration}`}</p>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
{event.metadata.app && (
<p>{`Target app: ${event.metadata.app}`}</p>
)}
{event.metadata.appId && (
<p>{`Target app: ${event.metadata.appId}`}</p>
)}
{event.metadata.targetEnvironment && (
<p>{`Target environment: ${event.metadata.targetEnvironment}`}</p>
)}
{event.metadata.targetEnvironmentId && (
<p>{`Target environment ID: ${event.metadata.targetEnvironmentId}`}</p>
)}
</Td>
);
case EventType.DELETE_INTEGRATION:
return (
<Td>
<p>{`Integration: ${event.metadata.integration}`}</p>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
{event.metadata.app && (
<p>{`Target App: ${event.metadata.app}`}</p>
)}
{event.metadata.appId && (
<p>{`Target app: ${event.metadata.appId}`}</p>
)}
{event.metadata.targetEnvironment && (
<p>{`Target environment: ${event.metadata.targetEnvironment}`}</p>
)}
{event.metadata.targetEnvironmentId && (
<p>{`Target environment ID: ${event.metadata.targetEnvironmentId}`}</p>
)}
</Td>
);
case EventType.ADD_TRUSTED_IP:
return (
<Td>
<p>{`IP: ${event.metadata.ipAddress}${event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""}`}</p>
</Td>
);
case EventType.UPDATE_TRUSTED_IP:
return (
<Td>
<p>{`IP: ${event.metadata.ipAddress}${event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""}`}</p>
</Td>
);
case EventType.DELETE_TRUSTED_IP:
return (
<Td>
<p>{`IP: ${event.metadata.ipAddress}${event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""}`}</p>
</Td>
);
case EventType.CREATE_SERVICE_TOKEN:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
</Td>
);
case EventType.DELETE_SERVICE_TOKEN:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
</Td>
);
case EventType.CREATE_ENVIRONMENT:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
<p>{`Slug: ${event.metadata.slug}`}</p>
</Td>
);
case EventType.UPDATE_ENVIRONMENT:
return (
<Td>
<p>{`Old name: ${event.metadata.oldName}`}</p>
<p>{`New name: ${event.metadata.newName}`}</p>
<p>{`Old slug: ${event.metadata.oldSlug}`}</p>
<p>{`New slug: ${event.metadata.newSlug}`}</p>
</Td>
);
case EventType.DELETE_ENVIRONMENT:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
<p>{`Slug: ${event.metadata.slug}`}</p>
</Td>
);
case EventType.ADD_WORKSPACE_MEMBER:
return (
<Td>
<p>{`Email: ${event.metadata.email}`}</p>
</Td>
);
case EventType.REMOVE_WORKSPACE_MEMBER:
return (
<Td>
<p>{`Email: ${event.metadata.email}`}</p>
</Td>
);
case EventType.CREATE_FOLDER:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.folderPath}`}</p>
<p>{`Folder: ${event.metadata.folderName}`}</p>
</Td>
);
case EventType.UPDATE_FOLDER:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.folderPath}`}</p>
<p>{`Old folder: ${event.metadata.oldFolderName}`}</p>
<p>{`New folder: ${event.metadata.newFolderName}`}</p>
</Td>
);
case EventType.DELETE_FOLDER:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.folderPath}`}</p>
<p>{`Folder: ${event.metadata.folderName}`}</p>
</Td>
);
case EventType.CREATE_WEBHOOK:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
<p>{`Webhook URL: ${event.metadata.webhookUrl}`}</p>
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
</Td>
);
case EventType.UPDATE_WEBHOOK_STATUS:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
<p>{`Webhook URL: ${event.metadata.webhookUrl}`}</p>
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
</Td>
);
case EventType.DELETE_WEBHOOK:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
<p>{`Webhook URL: ${event.metadata.webhookUrl}`}</p>
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
</Td>
);
case EventType.GET_SECRET_IMPORTS:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`# Imported paths: ${event.metadata.numberOfImports}`}</p>
</Td>
);
case EventType.CREATE_SECRET_IMPORT:
return (
<Td>
<p>{`Import from env: ${event.metadata.importFromEnvironment}`}</p>
<p>{`Import from path: ${event.metadata.importFromSecretPath}`}</p>
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
</Td>
);
case EventType.UPDATE_SECRET_IMPORT:
return (
<Td>
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
</Td>
);
case EventType.DELETE_SECRET_IMPORT:
return (
<Td>
<p>{`Import from env: ${event.metadata.importFromEnvironment}`}</p>
<p>{`Import from path: ${event.metadata.importFromSecretPath}`}</p>
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
</Td>
);
case EventType.UPDATE_USER_WORKSPACE_ROLE:
return (
<Td>
<p>{`Email: ${event.metadata.email}`}</p>
<p>{`Old role: ${event.metadata.oldRole}`}</p>
<p>{`New role: ${event.metadata.newRole}`}</p>
</Td>
);
case EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS:
return (
<Td>
<p>{`Email: ${event.metadata.email}`}</p>
{event.metadata.deniedPermissions.map((permission) => {
return (
<p key={`audit-log-denied-permission-${event.metadata.userId}-${permission.environmentSlug}-${permission.ability}`}>
{`Denied env-ability: ${permission.environmentSlug}-${permission.ability}`}
</p>
);
})}
</Td>
);
default:
return (
<Td />
);
}
}
const formatDate = (dateToFormat: string) => {
const date = new Date(dateToFormat);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
let hours = date.getHours();
const minutes = String(date.getMinutes()).padStart(2, "0");
// convert from 24h to 12h format
const period = hours >= 12 ? "PM" : "AM";
hours %= 12;
hours = hours || 12; // the hour '0' should be '12'
const formattedDate = `${day}-${month}-${year} at ${hours}:${minutes} ${period}`;
return formattedDate;
}
return (
<Tr className={`log-${auditLog._id} h-10 border-b border-x-0 border-t-0`}>
<Td>{formatDate(auditLog.createdAt)}</Td>
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
{renderActor(auditLog.actor)}
<Td>
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
<p>{auditLog.ipAddress}</p>
</Td>
{renderMetadata(auditLog.event)}
</Tr>
);
}
@@ -0,0 +1 @@
export { LogsSection } from "./LogsSection";
@@ -0,0 +1,26 @@
import * as yup from "yup";
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
export const auditLogFilterFormSchema = yup.object({
eventType: yup.string()
.oneOf(Object.values(EventType), "Invalid event type"),
actor: yup.string(),
userAgentType: yup.string()
.oneOf(Object.values(UserAgentType), "Invalid user agent type"),
startDate: yup.date(),
endDate: yup.date().min(yup.ref("startDate"), "End date cannot be before start date"),
page: yup.number(),
perPage: yup.number()
}).required();
export type AuditLogFilterFormData = yup.InferType<typeof auditLogFilterFormSchema>;
export type SetValueType = (
name: keyof AuditLogFilterFormData,
value: any,
options?: {
shouldValidate?: boolean,
shouldDirty?: boolean
}
) => void;
@@ -0,0 +1 @@
export { AuditLogsPage } from "./AuditLogsPage";
@@ -288,8 +288,14 @@ export const SecretOverviewPage = () => {
className="min-table-row min-w-[11rem] border-b-0 p-0 text-center"
key={`secret-overview-${name}-${index + 1}`}
>
<div className="flex items-center justify-center border-b border-mineshaft-600 px-5 pt-4 pb-3.5">
{name}
<div className="flex items-center justify-center border-b border-mineshaft-600 px-5 pt-3.5 pb-3">
<button
type="button"
className="font-medium hover:text-mineshaft-100 duration-100 text-sm"
onClick={() => handleExploreEnvClick(slug)}
>
{name}
</button>
{missingKeyCount > 0 && (
<Tooltip
className="max-w-none lowercase"
@@ -1,46 +0,0 @@
import { useRouter } from "next/router";
import NavHeader from "@app/components/navigation/NavHeader";
import { SAProjectLevelPermissionsTable } from "./components/SAProjectLevelPermissionsTable";
import {
CopyServiceAccountPublicKeySection,
ServiceAccountNameChangeSection
} from "./components";
export const CreateServiceAccountPage = () => {
const router = useRouter();
const {serviceAccountId} = router.query;
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<NavHeader
pageName="Service Account"
isOrganizationRelated
/>
<div className="my-8 ml-6 max-w-5xl">
<p className="text-3xl font-semibold text-gray-200">Service Account</p>
<p className="text-base font-normal text-gray-400">
A service account represents a machine identity such as a VM or application client.
</p>
</div>
{typeof serviceAccountId === "string" && (
<div className="max-w-8xl mx-6">
<ServiceAccountNameChangeSection
serviceAccountId={serviceAccountId}
/>
<div className="mt-8">
<CopyServiceAccountPublicKeySection
serviceAccountId={serviceAccountId}
/>
</div>
<div className="mt-8">
<SAProjectLevelPermissionsTable
serviceAccountId={serviceAccountId}
/>
</div>
</div>
)}
</div>
);
}
@@ -1,49 +0,0 @@
import { useEffect } from "react";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconButton } from "@app/components/v2";
import { useToggle } from "@app/hooks";
type Props = {
serviceAccountId: string;
}
export const CopyServiceAccountIDSection = ({ serviceAccountId }: Props): JSX.Element => {
const [isServiceAccountIdCopied, setIsServiceAccountIdCopied] = useToggle(false);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isServiceAccountIdCopied) {
timer = setTimeout(() => setIsServiceAccountIdCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isServiceAccountIdCopied]);
const copyServiceAccountIdToClipboard = () => {
navigator.clipboard.writeText(serviceAccountId);
setIsServiceAccountIdCopied.on();
};
return (
<div className="flex w-full flex-col items-start rounded-md bg-white/5 px-6 p-6">
<p className="text-xl font-semibold">Service Account ID</p>
<div className="mt-4 flex items-center justify-end rounded-md bg-white/[0.07] text-base text-gray-400 p-2">
<p className="mr-4 break-all">{serviceAccountId}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => copyServiceAccountIdToClipboard()}
>
<FontAwesomeIcon icon={isServiceAccountIdCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Copy
</span>
</IconButton>
</div>
</div>
);
}
@@ -1 +0,0 @@
export { CopyServiceAccountIDSection } from "./CopyServiceAccountIDSection";
@@ -1,53 +0,0 @@
import { useEffect } from "react";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconButton } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { useGetServiceAccountById } from "@app/hooks/api";
type Props = {
serviceAccountId: string;
}
export const CopyServiceAccountPublicKeySection = ({ serviceAccountId }: Props): JSX.Element => {
const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId);
const [isServiceAccountIdCopied, setIsServiceAccountIdCopied] = useToggle(false);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isServiceAccountIdCopied) {
timer = setTimeout(() => setIsServiceAccountIdCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isServiceAccountIdCopied]);
const copyServiceAccountIdToClipboard = () => {
if (!serviceAccount) return;
navigator.clipboard.writeText(serviceAccount.publicKey);
setIsServiceAccountIdCopied.on();
};
return serviceAccount ? (
<div className="flex w-full flex-col items-start rounded-md bg-white/5 px-6 p-6">
<p className="text-xl font-semibold">Public Key</p>
<div className="mt-4 flex items-center justify-end rounded-md bg-white/[0.07] text-base text-gray-400 p-2">
<p className="mr-4 break-all">{serviceAccount.publicKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => copyServiceAccountIdToClipboard()}
>
<FontAwesomeIcon icon={isServiceAccountIdCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Copy
</span>
</IconButton>
</div>
</div>
) : <div />
}
@@ -1 +0,0 @@
export { CopyServiceAccountPublicKeySection } from "./CopyServiceAccountPublicKeySection";
@@ -1,412 +0,0 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faKey, faMagnifyingGlass, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import {
decryptAssymmetric,
encryptAssymmetric,
verifyPrivateKey
} from "@app/components/utilities/cryptography/crypto";
import {
Button,
Checkbox,
DeleteActionModal,
EmptyState,
FormControl,
IconButton,
Input,
Modal,
ModalClose,
ModalContent,
Select,
SelectItem,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import {
useCreateServiceAccountProjectLevelPermission,
useDeleteServiceAccountProjectLevelPermission,
useGetServiceAccountById,
useGetServiceAccountProjectLevelPermissions,
useGetUserWorkspaces
} from "@app/hooks/api";
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
const createProjectLevelPermissionSchema = yup.object({
privateKey: yup.string().required().label("Private Key"),
workspace: yup.string().required().label("Workspace"),
environment: yup.string().required().label("Environment"),
permissions: yup
.object()
.shape({
read: yup.boolean().required(),
write: yup.boolean().required()
})
.defined()
.required()
});
type CreateProjectLevelPermissionForm = yup.InferType<typeof createProjectLevelPermissionSchema>;
type Props = {
serviceAccountId: string;
};
export const SAProjectLevelPermissionsTable = ({ serviceAccountId }: Props): JSX.Element => {
const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId);
const { data: userWorkspaces, isLoading: isUserWorkspacesLoading } = useGetUserWorkspaces();
const [searchPermissions, setSearchPermissions] = useState("");
const { data: serviceAccountWorkspacePermissions, isLoading: isPermissionsLoading } =
useGetServiceAccountProjectLevelPermissions(serviceAccountId);
const createServiceAccountProjectLevelPermission =
useCreateServiceAccountProjectLevelPermission();
const deleteServiceAccountProjectLevelPermission =
useDeleteServiceAccountProjectLevelPermission();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addProjectLevelPermission",
"removeProjectLevelPermission"
] as const);
const [, setSelectedWorkspace] = useState<undefined | string>(undefined);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<CreateProjectLevelPermissionForm>({
resolver: yupResolver(createProjectLevelPermissionSchema)
});
const onAddProjectLevelPermission = async ({
privateKey,
workspace,
environment,
permissions: { read, write }
}: CreateProjectLevelPermissionForm) => {
// TODO: clean up / modularize this function
if (!serviceAccount) return;
const { latestKey } = await getLatestFileKey({
workspaceId: workspace
});
verifyPrivateKey({
privateKey,
publicKey: serviceAccount.publicKey
});
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key,
publicKey: serviceAccount.publicKey,
privateKey
});
await createServiceAccountProjectLevelPermission.mutateAsync({
serviceAccountId,
workspaceId: workspace,
environment,
read,
write,
encryptedKey: ciphertext,
nonce
});
handlePopUpClose("addProjectLevelPermission");
};
const onRemoveProjectLevelPermission = async () => {
const serviceAccountWorkspacePermissionId = (
popUp?.removeProjectLevelPermission?.data as { _id: string }
)?._id;
await deleteServiceAccountProjectLevelPermission.mutateAsync({
serviceAccountId,
serviceAccountWorkspacePermissionId
});
handlePopUpClose("removeProjectLevelPermission");
};
return (
<div className="w-full bg-white/5 p-6">
<p className="mb-4 text-xl font-semibold">Project-Level Permissions</p>
<div className="mb-4 flex">
<div className="mr-4 flex-1">
<Input
value={searchPermissions}
onChange={(e) => setSearchPermissions(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search service account project-level permissions..."
/>
</div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
handlePopUpOpen("addProjectLevelPermission");
reset();
}}
>
Add Permission
</Button>
</div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Project</Th>
<Th>Environment</Th>
<Th>Read</Th>
<Th>Write</Th>
<Th aria-label="actions" />
</Tr>
</THead>
<TBody>
{isPermissionsLoading && (
<TableSkeleton columns={6} innerKey="service-account-project-level-permissions" />
)}
{!isPermissionsLoading &&
serviceAccountWorkspacePermissions &&
serviceAccountWorkspacePermissions.map(
({ _id, workspace, environment, read, write }) => {
const environmentName = workspace.environments.find(
(env) => env.slug === environment
)?.name;
return (
<Tr key={`service-account-project-level-permission-${_id}`} className="w-full">
<Td>{workspace.name}</Td>
<Td>{environmentName}</Td>
<Td>
<Checkbox id="isReadPermissionEnabled" isChecked={read} isDisabled>
{/**/}
</Checkbox>
</Td>
<Td>
<Checkbox id="isWritePermissionEnabled" isChecked={write} isDisabled>
{/**/}
</Checkbox>
</Td>
<Td>
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen("removeProjectLevelPermission", { _id })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Td>
</Tr>
);
}
)}
{!isPermissionsLoading && serviceAccountWorkspacePermissions?.length === 0 && (
<Tr>
<Td colSpan={7} className="py-6 text-center text-bunker-400">
<EmptyState title="No permissions found" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
<Modal
isOpen={popUp?.addProjectLevelPermission?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addProjectLevelPermission", isOpen);
}}
>
<ModalContent
title="Add a Project-Level Permission"
subTitle="The service account will be granted scoped access to the specified project and environment"
>
<form onSubmit={handleSubmit(onAddProjectLevelPermission)}>
{!isUserWorkspacesLoading && userWorkspaces && (
<>
<Controller
control={control}
defaultValue=""
name="privateKey"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Service Account Private Key"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="workspace"
defaultValue={String(userWorkspaces?.[0]?._id)}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Project" errorText={error?.message}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
onChange(e);
setSelectedWorkspace(e);
}}
className="border-mine-shaft-500 w-full border"
>
{userWorkspaces && userWorkspaces.length > 0 ? (
userWorkspaces.map((userWorkspace) => {
return (
<SelectItem
value={userWorkspace._id}
key={`project-${userWorkspace._id}`}
>
{userWorkspace.name}
</SelectItem>
);
})
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="environment"
defaultValue={String(userWorkspaces?.[0]?.environments?.[0]?.slug)}
render={({ field: { onChange, ...field } }) => {
const environments =
userWorkspaces?.find(
/* eslint-disable-next-line no-underscore-dangle */
(userWorkspace) => userWorkspace._id === control?._formValues?.workspace
)?.environments ?? [];
return (
<FormControl label="Environment" className="mt-4">
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="border-mine-shaft-500 w-full border"
>
{environments.length > 0 ? (
environments.map((environment) => {
return (
<SelectItem
value={environment.slug}
key={`environment-${environment.slug}`}
>
{environment.name}
</SelectItem>
);
})
) : (
<SelectItem value="none" key="target-app-none">
No environments found
</SelectItem>
)}
</Select>
</FormControl>
);
}}
/>
</>
)}
<Controller
control={control}
name="permissions"
defaultValue={{
read: true,
write: false
}}
render={({ field: { onChange, value }, fieldState: { error } }) => {
const options = [
{
label: "Read (default)",
value: "read"
},
{
label: "Write",
value: "write"
}
];
return (
<FormControl
label="Permissions"
errorText={error?.message}
isError={Boolean(error)}
>
<>
{options.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={value[optionValue]}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
isDisabled={optionValue === "read"}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</>
</FormControl>
);
}}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
type="submit"
isDisabled={isSubmitting}
isLoading={isSubmitting}
>
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeProjectLevelPermission.isOpen}
deleteKey="remove"
title="Do you want to remove this permission from the service account?"
onChange={(isOpen) => handlePopUpToggle("removeProjectLevelPermission", isOpen)}
onDeleteApproved={onRemoveProjectLevelPermission}
/>
</div>
);
};
@@ -1 +0,0 @@
export { SAProjectLevelPermissionsTable } from "./SAProjectLevelPermissionsTable";
@@ -1,88 +0,0 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import {
Button,
FormControl,
Input} from "@app/components/v2";
import {
useGetServiceAccountById,
useRenameServiceAccount
} from "@app/hooks/api";
const formSchema = yup.object({
name: yup.string().required().label("Service Account Name")
});
type FormData = yup.InferType<typeof formSchema>;
type Props = {
serviceAccountId: string;
}
export const ServiceAccountNameChangeSection = ({
serviceAccountId
}: Props) => {
const { data: serviceAccount, isLoading: isServiceAccountLoading } = useGetServiceAccountById(serviceAccountId);
const renameServiceAccount = useRenameServiceAccount();
const {
handleSubmit,
control,
reset,
formState: { isDirty, isSubmitting }
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
useEffect(() => {
reset({ name: serviceAccount?.name });
}, [serviceAccount?.name]);
const onFormSubmit = async ({ name }: FormData) => {
try {
await renameServiceAccount.mutateAsync({
serviceAccountId,
name
});
} catch (err) {
console.error(err);
}
}
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="rounded-md bg-white/5 p-6"
>
<p className="mb-4 text-xl font-semibold">Name</p>
<div className="mb-2 w-full max-w-lg">
{!isServiceAccountLoading && (
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input placeholder="Type your service account name..." {...field} />
</FormControl>
)}
control={control}
name="name"
/>
)}
</div>
<Button
isLoading={isSubmitting}
color="mineshaft"
size="sm"
type="submit"
isDisabled={!isDirty || isSubmitting}
leftIcon={<FontAwesomeIcon icon={faCheck} />}
>
Save Changes
</Button>
</form>
);
}
@@ -1 +0,0 @@
export { ServiceAccountNameChangeSection } from "./ServiceAccountNameChangeSection";
@@ -1,4 +0,0 @@
export { CopyServiceAccountIDSection } from "./CopyServiceAccountIDSection";
export { CopyServiceAccountPublicKeySection } from "./CopyServiceAccountPublicKeySection";
export { SAProjectLevelPermissionsTable } from "./SAProjectLevelPermissionsTable";
export { ServiceAccountNameChangeSection } from "./ServiceAccountNameChangeSection";
@@ -1 +0,0 @@
export { CreateServiceAccountPage } from "./CreateServiceAccountPage";
@@ -1,17 +1,14 @@
import { useEffect, useState } from "react";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Checkbox, EmailServiceSetupModal } from "@app/components/v2";
import {
useGetUser,
useUpdateMfaEnabled} from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { usePopUp } from "@app/hooks/usePopUp";
import { useGetUser } from "../../../../hooks/api";
import { User } from "../../../../hooks/api/types";
import updateMyMfaEnabled from "../../../../pages/api/user/updateMyMfaEnabled";
export const MFASection = () => {
const [isMfaEnabled, setIsMfaEnabled] = useState(false);
const { data: user } = useGetUser();
const { mutateAsync } = useUpdateMfaEnabled();
const { createNotification } = useNotificationContext();
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([
"setUpEmail"
@@ -19,22 +16,12 @@ export const MFASection = () => {
const {data: serverDetails } = useFetchServerStatus()
useEffect(() => {
if (user && typeof user.isMfaEnabled !== "undefined") {
setIsMfaEnabled(user.isMfaEnabled);
}
}, [user]);
const toggleMfa = async (state: boolean) => {
try {
const newUser: User = await updateMyMfaEnabled({
const newUser = await mutateAsync({
isMfaEnabled: state
});
if (newUser) {
setIsMfaEnabled(newUser.isMfaEnabled);
}
createNotification({
text: `${newUser.isMfaEnabled ? "Successfully turned on two-factor authentication." : "Successfully turned off two-factor authentication."}`,
type: "success"
@@ -55,20 +42,22 @@ export const MFASection = () => {
<p className="text-xl font-semibold text-mineshaft-100 mb-8">
Two-factor Authentication
</p>
<Checkbox
className="data-[state=checked]:bg-primary"
id="isTwoFAEnabled"
isChecked={isMfaEnabled}
onCheckedChange={(state) => {
if (serverDetails?.emailConfigured){
toggleMfa(state as boolean);
} else {
handlePopUpOpen("setUpEmail");
}
}}
>
Enable 2-factor authentication via your personal email.
</Checkbox>
{user && (
<Checkbox
className="data-[state=checked]:bg-primary"
id="isTwoFAEnabled"
isChecked={user?.isMfaEnabled}
onCheckedChange={(state) => {
if (serverDetails?.emailConfigured){
toggleMfa(state as boolean);
} else {
handlePopUpOpen("setUpEmail");
}
}}
>
Enable 2-factor authentication via your personal email.
</Checkbox>
)}
</div>
</form>
<EmailServiceSetupModal
@@ -17,7 +17,7 @@ const tabs = [
export const ProjectSettingsPage = () => {
const { t } = useTranslation();
return (
<div className="flex h-full w-full justify-center bg-bunker-800 px-6 text-white">
<div className="flex w-full justify-center bg-bunker-800 px-6 text-white">
<div className="w-full max-w-screen-lg">
<div className="relative right-5 ml-4">
<NavHeader pageName={t("settings.project.title")} isProjectRelated />
@@ -1,30 +1,16 @@
import { useEffect, useState } from "react";
import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import { Checkbox } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import getBot from "../../../../../pages/api/bot/getBot";
import setBotActiveStatus from "../../../../../pages/api/bot/setBotActiveStatus";
import getLatestFileKey from "../../../../../pages/api/workspace/getLatestFileKey";
import { useGetUserWsKey,useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
export const E2EESection = () => {
const { currentWorkspace } = useWorkspace();
const [bot, setBot] = useState<any>(null);
useEffect(() => {
(async () => {
if (currentWorkspace) {
// get project bot
setBot(await getBot({
workspaceId: currentWorkspace._id
}));
}
})();
}, [currentWorkspace]);
const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? "");
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
/**
* Activate bot for project by performing the following steps:
@@ -38,14 +24,12 @@ export const E2EESection = () => {
try {
if (!currentWorkspace?._id) return;
if (bot) {
if (bot && wsKey) {
// case: there is a bot
if (!bot.isActive) {
// bot is not active -> activate bot
const key = await getLatestFileKey({
workspaceId: currentWorkspace._id
});
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) {
@@ -53,9 +37,9 @@ export const E2EESection = () => {
}
const WORKSPACE_KEY = decryptAssymmetric({
ciphertext: key.latestKey.encryptedKey,
nonce: key.latestKey.nonce,
publicKey: key.latestKey.sender.publicKey,
ciphertext: wsKey.encryptedKey,
nonce: wsKey.nonce,
publicKey: wsKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
@@ -69,22 +53,20 @@ export const E2EESection = () => {
encryptedKey: ciphertext,
nonce
};
const botx = await setBotActiveStatus({
botId: bot._id,
await updateBotActiveStatus({
workspaceId: currentWorkspace._id,
botKey,
isActive: true,
botKey
botId: bot._id
});
setBot(botx.bot);
} else {
// bot is active -> deactivate bot
const botx = await setBotActiveStatus({
await updateBotActiveStatus({
isActive: false,
botId: bot._id,
isActive: false
workspaceId: currentWorkspace._id
});
setBot(botx.bot);
}
}
} catch (err) {
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import { faInfoCircle, faPlug, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import dayjs from "dayjs";
import { format } from "date-fns";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
@@ -196,7 +196,8 @@ export const WebhooksTab = () => {
content={
<div className="text-xs">
<div>
Updated At: {dayjs(updatedAt).format("YYYY-MM-DD, hh:mm A")}
Updated At:{" "}
{format(new Date(updatedAt), "YYYY-MM-DD, hh:mm aaa")}
</div>
{lastRunErrorMessage && (
<div className="mt-2 text-red">
@@ -17,8 +17,8 @@ import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLo
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input } from "@app/components/v2";
import { useGetCommonPasswords } from "@app/hooks/api";
import completeAccountInformationSignup from "@app/pages/api/auth/CompleteAccountInformationSignup";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import { completeAccountSignup } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import ProjectService from "@app/services/ProjectService";
// eslint-disable-next-line new-cap
@@ -157,7 +157,7 @@ export const UserInfoSSOStep = ({
secret: Buffer.from(derivedKey.hash)
});
const response = await completeAccountInformationSignup({
const response = await completeAccountSignup({
email,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),
@@ -188,7 +188,7 @@ export const UserInfoSSOStep = ({
privateKey
});
const userOrgs = await getOrganizations();
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]?._id;
const project = await ProjectService.initProject({
organizationId: orgId,