Merge remote-tracking branch 'origin' into groups-phase-2c

This commit is contained in:
Tuan Dang
2024-05-06 09:33:36 -07:00
67 changed files with 1969 additions and 537 deletions
@@ -38,12 +38,7 @@ type ReferenceItem = {
export const InfisicalSecretInput = ({
value: propValue,
isVisible,
containerClassName,
onBlur,
isDisabled,
isImport,
isReadOnly,
secretPath: propSecretPath,
environment: propEnvironment,
onChange,
@@ -276,10 +271,7 @@ export const InfisicalSecretInput = ({
handleSuggestionSelect();
}
if (
(["ArrowDown", "ArrowUp"].includes(e.key) && isPopupOpen) ||
(e.key === "Enter" && highlightedIndex >= 0)
) {
if (["ArrowDown", "ArrowUp", "Enter"].includes(e.key) && isPopupOpen) {
e.preventDefault();
}
};
@@ -0,0 +1,6 @@
export {
useCreateAuditLogStream,
useDeleteAuditLogStream,
useUpdateAuditLogStream
} from "./mutations";
export { useGetAuditLogStreamDetails, useGetAuditLogStreams } from "./queries";
@@ -0,0 +1,61 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { auditLogStreamKeys } from "./queries";
import {
TAuditLogStream,
TCreateAuditLogStreamDTO,
TDeleteAuditLogStreamDTO,
TUpdateAuditLogStreamDTO
} from "./types";
export const useCreateAuditLogStream = () => {
const queryClient = useQueryClient();
return useMutation<{ auditLogStream: TAuditLogStream }, {}, TCreateAuditLogStreamDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.post<{ auditLogStream: TAuditLogStream }>(
"/api/v1/audit-log-streams",
dto
);
return data;
},
onSuccess: (_, { orgId }) => {
queryClient.invalidateQueries(auditLogStreamKeys.list(orgId));
}
});
};
export const useUpdateAuditLogStream = () => {
const queryClient = useQueryClient();
return useMutation<{ auditLogStream: TAuditLogStream }, {}, TUpdateAuditLogStreamDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.patch<{ auditLogStream: TAuditLogStream }>(
`/api/v1/audit-log-streams/${dto.id}`,
dto
);
return data;
},
onSuccess: (_, { orgId }) => {
queryClient.invalidateQueries(auditLogStreamKeys.list(orgId));
}
});
};
export const useDeleteAuditLogStream = () => {
const queryClient = useQueryClient();
return useMutation<{ auditLogStream: TAuditLogStream }, {}, TDeleteAuditLogStreamDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.delete<{ auditLogStream: TAuditLogStream }>(
`/api/v1/audit-log-streams/${dto.id}`
);
return data;
},
onSuccess: (_, { orgId }) => {
queryClient.invalidateQueries(auditLogStreamKeys.list(orgId));
}
});
};
@@ -0,0 +1,40 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TAuditLogStream } from "./types";
export const auditLogStreamKeys = {
list: (orgId: string) => ["audit-log-stream", { orgId }],
getById: (id: string) => ["audit-log-stream-details", { id }]
};
const fetchAuditLogStreams = async () => {
const { data } = await apiRequest.get<{ auditLogStreams: TAuditLogStream[] }>(
"/api/v1/audit-log-streams"
);
return data.auditLogStreams;
};
export const useGetAuditLogStreams = (orgId: string) =>
useQuery({
queryKey: auditLogStreamKeys.list(orgId),
queryFn: () => fetchAuditLogStreams(),
enabled: Boolean(orgId)
});
const fetchAuditLogStreamDetails = async (id: string) => {
const { data } = await apiRequest.get<{ auditLogStream: TAuditLogStream }>(
`/api/v1/audit-log-streams/${id}`
);
return data.auditLogStream;
};
export const useGetAuditLogStreamDetails = (id: string) =>
useQuery({
queryKey: auditLogStreamKeys.getById(id),
queryFn: () => fetchAuditLogStreamDetails(id),
enabled: Boolean(id)
});
@@ -0,0 +1,28 @@
export type LogStreamHeaders = {
key: string;
value: string;
};
export type TAuditLogStream = {
id: string;
url: string;
headers?: LogStreamHeaders[];
};
export type TCreateAuditLogStreamDTO = {
url: string;
headers?: LogStreamHeaders[];
orgId: string;
};
export type TUpdateAuditLogStreamDTO = {
id: string;
url?: string;
headers?: LogStreamHeaders[];
orgId: string;
};
export type TDeleteAuditLogStreamDTO = {
id: string;
orgId: string;
};
+1
View File
@@ -1,6 +1,7 @@
export * from "./admin";
export * from "./apiKeys";
export * from "./auditLogs";
export * from "./auditLogStreams";
export * from "./auth";
export * from "./bots";
export * from "./dynamicSecret";
@@ -5,6 +5,8 @@ export type SubscriptionPlan = {
auditLogs: boolean;
dynamicSecret: boolean;
auditLogsRetentionDays: number;
auditLogStreamLimit: number;
auditLogStreams: boolean;
customAlerts: boolean;
customRateLimits: boolean;
pitRecovery: boolean;
+9 -8
View File
@@ -1,5 +1,6 @@
import { ZodIssue } from "zod";
export type { TAuditLogStream } from "./auditLogStreams/types";
export type { GetAuthTokenAPI } from "./auth/types";
export type { IncidentContact } from "./incidentContacts/types";
export type { IntegrationAuth } from "./integrationAuth/types";
@@ -48,13 +49,13 @@ export enum ApiErrorTypes {
export type TApiErrors =
| {
error: ApiErrorTypes.ValidationError;
message: ZodIssue[];
statusCode: 403;
}
error: ApiErrorTypes.ValidationError;
message: ZodIssue[];
statusCode: 403;
}
| { error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 401 }
| {
statusCode: 400;
message: string;
error: ApiErrorTypes.BadRequestError;
};
statusCode: 400;
message: string;
error: ApiErrorTypes.BadRequestError;
};
@@ -594,7 +594,7 @@ const OrganizationPage = withPermission(
</div>
)}
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
{window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("http://localhost:8080") && (
{(window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("http://localhost:8080")) && (
<div
className={`${
!updateClosed ? "block" : "hidden"
@@ -602,10 +602,10 @@ const OrganizationPage = withPermission(
>
<FontAwesomeIcon icon={faWarning} className="p-6 text-4xl text-primary" />
<div className="text-sm">
<span className="text-lg font-semibold">Scheduled maintenance on April 13th 2024 </span>{" "}
<span className="text-lg font-semibold">Scheduled maintenance on May 11th 2024 </span>{" "}
<br />
Infisical will undergo scheduled maintenance for approximately 1 hour on Saturday, April 13th, 11am EST. During these hours, read
operations will continue to function normally but no resources will be editable.
Infisical will undergo scheduled maintenance for approximately 2 hour on Saturday, May 11th, 11am EST. During these hours, read
operations to Infisical will continue to function normally but no resources will be editable.
No action is required on your end — your applications will continue to fetch secrets.
<br />
</div>
@@ -1,3 +1,5 @@
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { ServiceTokenSection } from "./components";
@@ -11,7 +13,37 @@ export const ServiceTokenTab = () => {
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<ServiceTokenSection />
<div className="space-y-3">
<div className="flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white">
<FontAwesomeIcon icon={faWarning} className="pr-6 text-4xl text-white/80" />
<div className="flex w-full flex-col text-sm">
<span className="mb-4 text-lg font-semibold">Deprecation Notice</span>
<p>
Service Tokens are being deprecated in favor of Machine Identities.
<br />
They will be removed in the future in accordance with the deprecation notice and
timeline stated{" "}
<a
href="https://infisical.com/blog/deprecating-api-keys"
target="_blank"
className="font-semibold text-primary-400" rel="noreferrer"
>
here
</a>
.
<br />
<a
href="https://infisical.com/docs/documentation/platform/identities/overview"
target="_blank"
className="font-semibold text-primary-400" rel="noreferrer"
>
Learn more about Machine Identities
</a>
</p>
</div>
</div>
<ServiceTokenSection />
</div>
</motion.div>
);
};
@@ -0,0 +1,206 @@
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, FormLabel, IconButton, Input, Spinner } from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useCreateAuditLogStream,
useGetAuditLogStreamDetails,
useUpdateAuditLogStream
} from "@app/hooks/api";
type Props = {
id?: string;
onClose: () => void;
};
const formSchema = z.object({
url: z.string().url().min(1),
headers: z
.object({
key: z.string(),
value: z.string()
})
.array()
.optional()
});
type TForm = z.infer<typeof formSchema>;
export const AuditLogStreamForm = ({ id = "", onClose }: Props) => {
const isEdit = Boolean(id);
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const auditLogStream = useGetAuditLogStreamDetails(id);
const createAuditLogStream = useCreateAuditLogStream();
const updateAuditLogStream = useUpdateAuditLogStream();
const {
handleSubmit,
control,
setValue,
getValues,
formState: { isSubmitting }
} = useForm<TForm>({
values: auditLogStream?.data,
defaultValues: {
headers: [{ key: "", value: "" }]
}
});
const headerFields = useFieldArray({
control,
name: "headers"
});
const handleAuditLogStreamEdit = async ({ headers, url }: TForm) => {
if (!id) return;
try {
await updateAuditLogStream.mutateAsync({
id,
orgId,
headers,
url
});
createNotification({
type: "success",
text: "Successfully updated stream"
});
onClose();
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to update stream"
});
}
};
const handleFormSubmit = async ({ headers = [], url }: TForm) => {
if (isSubmitting) return;
const sanitizedHeaders = headers.filter(({ key, value }) => Boolean(key) && Boolean(value));
const streamHeaders = sanitizedHeaders.length ? sanitizedHeaders : undefined;
if (isEdit) {
await handleAuditLogStreamEdit({ headers: streamHeaders, url });
return;
}
try {
await createAuditLogStream.mutateAsync({
orgId,
headers: streamHeaders,
url
});
createNotification({
type: "success",
text: "Successfully created stream"
});
onClose();
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to create stream"
});
}
};
if (isEdit && auditLogStream.isLoading) {
return (
<div className="flex items-center justify-center p-8">
<Spinner size="lg" />
</div>
);
}
return (
<form onSubmit={handleSubmit(handleFormSubmit)} autoComplete="off">
<div>
<Controller
control={control}
name="url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Endpoint URL"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<FormLabel label="Headers" isOptional />
{headerFields.fields.map(({ id: headerFieldId }, i) => (
<div key={headerFieldId} className="flex space-x-2">
<Controller
control={control}
name={`headers.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-1/3"
>
<Input {...field} placeholder="Authorization" />
</FormControl>
)}
/>
<Controller
control={control}
name={`headers.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="flex-grow"
>
<Input
{...field}
type="password"
placeholder="Bearer <token>"
autoComplete="new-password"
/>
</FormControl>
)}
/>
<IconButton
ariaLabel="delete key"
className="h-9"
variant="outline_bg"
onClick={() => {
const header = getValues("headers");
if (header && header?.length > 1) {
headerFields.remove(i);
} else {
setValue("headers", [{ key: "", value: "" }]);
}
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => headerFields.append({ value: "", key: "" })}
>
Add Key
</Button>
</div>
</div>
<div className="mt-8 flex items-center">
<Button className="mr-4" type="submit" isLoading={isSubmitting}>
{isEdit ? "Save" : "Create"}
</Button>
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
Cancel
</Button>
</div>
</form>
);
};
@@ -0,0 +1,197 @@
import { faPlug, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
EmptyState,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
THead,
Tr,
UpgradePlanModal
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { useDeleteAuditLogStream, useGetAuditLogStreams } from "@app/hooks/api";
import { AuditLogStreamForm } from "./AuditLogStreamForm";
export const AuditLogStreamsTab = withPermission(
() => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"auditLogStreamForm",
"deleteAuditLogStream",
"upgradePlan"
] as const);
const { subscription } = useSubscription();
const { data: auditLogStreams, isLoading: isAuditLogStreamsLoading } =
useGetAuditLogStreams(orgId);
// mutation
const { mutateAsync: deleteAuditLogStream } = useDeleteAuditLogStream();
const handleAuditLogStreamDelete = async () => {
try {
const auditLogStreamId = popUp?.deleteAuditLogStream?.data as string;
await deleteAuditLogStream({
id: auditLogStreamId,
orgId
});
handlePopUpClose("deleteAuditLogStream");
createNotification({
type: "success",
text: "Successfully deleted stream"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to delete stream"
});
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Audit Log Streams</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Button
onClick={() => {
if (subscription && !subscription?.auditLogStreams) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("auditLogStreamForm");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed}
>
Create
</Button>
)}
</OrgPermissionCan>
</div>
<p className="mb-8 text-gray-400">
Send audit logs from Infisical to external logging providers via HTTP
</p>
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Td>URL</Td>
<Td className="text-right">Action</Td>
</Tr>
</THead>
<TBody>
{isAuditLogStreamsLoading && (
<TableSkeleton columns={2} innerKey="stream-loading" />
)}
{!isAuditLogStreamsLoading && auditLogStreams && auditLogStreams?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No audit log streams found" icon={faPlug} />
</Td>
</Tr>
)}
{!isAuditLogStreamsLoading &&
auditLogStreams?.map(({ id, url }) => (
<Tr key={id}>
<Td className="max-w-xs overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
{url}
</Td>
<Td>
<div className="flex items-center justify-end space-x-2">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<Button
variant="outline_bg"
size="xs"
isDisabled={!isAllowed}
onClick={() => handlePopUpOpen("auditLogStreamForm", id)}
>
Edit
</Button>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<Button
variant="outline_bg"
className="border-red-800 bg-red-800 hover:border-red-700 hover:bg-red-700"
colorSchema="danger"
size="xs"
isDisabled={!isAllowed}
onClick={() => handlePopUpOpen("deleteAuditLogStream", id)}
>
Delete
</Button>
)}
</OrgPermissionCan>
</div>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
</div>
<Modal
isOpen={popUp.auditLogStreamForm.isOpen}
onOpenChange={(isModalOpen) => {
handlePopUpToggle("auditLogStreamForm", isModalOpen);
}}
>
<ModalContent
title={`${popUp?.auditLogStreamForm?.data ? "Update" : "Create"} Audit Log Stream `}
subTitle="Continuously stream logs from Infisical to third-party logging providers."
>
<AuditLogStreamForm
id={popUp?.auditLogStreamForm?.data as string}
onClose={() => handlePopUpToggle("auditLogStreamForm")}
/>
</ModalContent>
</Modal>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add audit log streams if you switch to Infisical's Enterprise plan."
/>
<DeleteActionModal
isOpen={popUp.deleteAuditLogStream.isOpen}
deleteKey="delete"
title="Are you sure you want to remove this stream?"
onChange={(isOpen) => handlePopUpToggle("deleteAuditLogStream", isOpen)}
onClose={() => handlePopUpClose("deleteAuditLogStream")}
onDeleteApproved={handleAuditLogStreamDelete}
/>
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Settings }
);
@@ -0,0 +1 @@
export { AuditLogStreamsTab } from "./AuditLogStreamTab";
@@ -1,12 +1,14 @@
import { Fragment } from "react";
import { Tab } from "@headlessui/react";
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
import { OrgAuthTab } from "../OrgAuthTab";
import { OrgGeneralTab } from "../OrgGeneralTab";
const tabs = [
{ name: "General", key: "tab-org-general" },
{ name: "Security", key: "tab-org-security" }
{ name: "Security", key: "tab-org-security" },
{ name: "Audit Log Streams", key: "tag-audit-log-streams" }
];
export const OrgTabGroup = () => {
return (
@@ -17,9 +19,8 @@ export const OrgTabGroup = () => {
{({ selected }) => (
<button
type="button"
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>
@@ -34,6 +35,9 @@ export const OrgTabGroup = () => {
<Tab.Panel>
<OrgAuthTab />
</Tab.Panel>
<Tab.Panel>
<AuditLogStreamsTab />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
);
@@ -1,5 +1,4 @@
import { Fragment } from "react";
import Link from "next/link";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tab } from "@headlessui/react";
@@ -47,18 +46,29 @@ export const PersonalTabGroup = () => {
<div className="flex w-full flex-col text-sm">
<span className="mb-4 text-lg font-semibold">Deprecation Notice</span>
<p>
API Keys are deprecated and will be removed in the future.
<br /> Please use Machine Identity authentication for your applications and
services.
</p>
<Link href="https://infisical.com/docs/documentation/platform/identities/overview">
<a target="_blank" className="font-semibold text-primary-400">
API Keys are being deprecated in favor of Machine Identities.
<br />
They will be removed in the future in accordance with the deprecation notice and
timeline stated{" "}
<a
href="https://infisical.com/blog/deprecating-api-keys"
target="_blank"
className="font-semibold text-primary-400" rel="noreferrer"
>
here
</a>
.
<br />
<a
href="https://infisical.com/docs/documentation/platform/identities/overview"
target="_blank"
className="font-semibold text-primary-400" rel="noreferrer"
>
Learn more about Machine Identities
</a>
</Link>
</p>
</div>
</div>
<PersonalAPIKeyTab />
</div>
</Tab.Panel>
@@ -7,7 +7,7 @@ import { WebhooksTab } from "./components/WebhooksTab";
const tabs = [
{ name: "General", key: "tab-project-general" },
{ name: "Webhooks", key: "tab-project-webhooks" }
{ name: "Webhooks", key: "tab-project-webhooks" },
];
export const ProjectSettingsPage = () => {
@@ -25,9 +25,8 @@ export const ProjectSettingsPage = () => {
{({ selected }) => (
<button
type="button"
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>