mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-17 01:12:59 +00:00
Fast forward
This commit is contained in:
@@ -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)!;
|
||||
|
||||
+3
-2
@@ -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
|
||||
});
|
||||
|
||||
|
||||
+21
-7
@@ -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}
|
||||
|
||||
+3
-1
@@ -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,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>
|
||||
);
|
||||
}
|
||||
-49
@@ -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
@@ -1 +0,0 @@
|
||||
export { CopyServiceAccountIDSection } from "./CopyServiceAccountIDSection";
|
||||
-53
@@ -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
@@ -1 +0,0 @@
|
||||
export { CopyServiceAccountPublicKeySection } from "./CopyServiceAccountPublicKeySection";
|
||||
-412
@@ -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
@@ -1 +0,0 @@
|
||||
export { SAProjectLevelPermissionsTable } from "./SAProjectLevelPermissionsTable";
|
||||
-88
@@ -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
@@ -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 />
|
||||
|
||||
+17
-35
@@ -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) {
|
||||
|
||||
+3
-2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user