mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 21:51:56 +00:00
Merge branch 'main' into feat/addSecretImportsToOverview
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
export {
|
||||
useAdminDeleteUser,
|
||||
useAdminGrantServerAdminAccess,
|
||||
useAdminRemoveIdentitySuperAdminAccess,
|
||||
useCreateAdminUser,
|
||||
useRemoveUserServerAdminAccess,
|
||||
useUpdateAdminSlackConfig,
|
||||
useUpdateServerConfig,
|
||||
useUpdateServerEncryptionStrategy
|
||||
|
||||
@@ -70,6 +70,40 @@ export const useAdminDeleteUser = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useAdminRemoveIdentitySuperAdminAccess = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (identityId: string) => {
|
||||
await apiRequest.delete(
|
||||
`/api/v1/admin/identity-management/identities/${identityId}/super-admin-access`
|
||||
);
|
||||
|
||||
return {};
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [adminStandaloneKeys.getIdentities]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useRemoveUserServerAdminAccess = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (userId: string) => {
|
||||
await apiRequest.delete(`/api/v1/admin/user-management/users/${userId}/admin-access`);
|
||||
|
||||
return {};
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [adminStandaloneKeys.getUsers]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useAdminGrantServerAdminAccess = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
|
||||
@@ -15,6 +15,7 @@ export type Identity = {
|
||||
authMethods: IdentityAuthMethod[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
isInstanceAdmin?: boolean;
|
||||
};
|
||||
|
||||
export type IdentityAccessToken = {
|
||||
|
||||
+28
-5
@@ -22,14 +22,37 @@ import "./translation";
|
||||
// have a look at the Quick start guide
|
||||
// for passing in lng and translations on init/
|
||||
|
||||
// https://vite.dev/guide/build#load-error-handling
|
||||
window.addEventListener("vite:preloadError", () => {
|
||||
window.location.reload(); // for example, refresh the page
|
||||
});
|
||||
|
||||
// Create a new router instance
|
||||
NProgress.configure({ showSpinner: false });
|
||||
|
||||
window.addEventListener("vite:preloadError", async (event) => {
|
||||
event.preventDefault();
|
||||
// Get current count from session storage or initialize to 0
|
||||
const reloadCount = parseInt(sessionStorage.getItem("vitePreloadErrorCount") || "0", 10);
|
||||
|
||||
// Check if we've already tried 3 times
|
||||
if (reloadCount >= 2) {
|
||||
console.warn("Vite preload has failed multiple times. Stopping automatic reload.");
|
||||
// Optionally show a user-facing message here
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if ("caches" in window) {
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(keys.map((key) => caches.delete(key)));
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.error(cleanupError);
|
||||
}
|
||||
//
|
||||
// Increment and save the counter
|
||||
sessionStorage.setItem("vitePreloadErrorCount", (reloadCount + 1).toString());
|
||||
|
||||
console.log(`Reloading page (attempt ${reloadCount + 1} of 2)...`);
|
||||
window.location.reload(); // for example, refresh the page
|
||||
});
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { serverConfig: null, queryClient },
|
||||
|
||||
@@ -59,8 +59,8 @@ const formSchema = z.object({
|
||||
trustLdapEmails: z.boolean(),
|
||||
trustOidcEmails: z.boolean(),
|
||||
defaultAuthOrgId: z.string(),
|
||||
authConsentContent: z.string().optional(),
|
||||
pageFrameContent: z.string().optional()
|
||||
authConsentContent: z.string().optional().default(""),
|
||||
pageFrameContent: z.string().optional().default("")
|
||||
});
|
||||
|
||||
type TDashboardForm = z.infer<typeof formSchema>;
|
||||
@@ -86,8 +86,8 @@ export const OverviewPage = () => {
|
||||
trustLdapEmails: config.trustLdapEmails,
|
||||
trustOidcEmails: config.trustOidcEmails,
|
||||
defaultAuthOrgId: config.defaultAuthOrgId ?? "",
|
||||
authConsentContent: config.authConsentContent,
|
||||
pageFrameContent: config.pageFrameContent
|
||||
authConsentContent: config.authConsentContent ?? "",
|
||||
pageFrameContent: config.pageFrameContent ?? ""
|
||||
}
|
||||
});
|
||||
|
||||
@@ -165,8 +165,8 @@ export const OverviewPage = () => {
|
||||
<Tab value={TabSections.Auth}>Authentication</Tab>
|
||||
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
|
||||
<Tab value={TabSections.Integrations}>Integrations</Tab>
|
||||
<Tab value={TabSections.Users}>Users</Tab>
|
||||
<Tab value={TabSections.Identities}>Identities</Tab>
|
||||
<Tab value={TabSections.Users}>User Identities</Tab>
|
||||
<Tab value={TabSections.Identities}>Machine Identities</Tab>
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Settings}>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { useState } from "react";
|
||||
import { faMagnifyingGlass, faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faEllipsis, faMagnifyingGlass, faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Input,
|
||||
Table,
|
||||
@@ -15,10 +22,22 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { useDebounce, usePopUp } from "@app/hooks";
|
||||
import { useAdminRemoveIdentitySuperAdminAccess } from "@app/hooks/api/admin";
|
||||
import { useAdminGetIdentities } from "@app/hooks/api/admin/queries";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const IdentityPanelTable = () => {
|
||||
const IdentityPanelTable = ({
|
||||
handlePopUpOpen
|
||||
}: {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["removeServerAdmin"]>,
|
||||
data?: {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
) => void;
|
||||
}) => {
|
||||
const [searchIdentityFilter, setSearchIdentityFilter] = useState("");
|
||||
const [debouncedSearchTerm] = useDebounce(searchIdentityFilter, 500);
|
||||
|
||||
@@ -48,15 +67,48 @@ const IdentityPanelTable = () => {
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={2} innerKey="identities" />}
|
||||
{!isPending &&
|
||||
data?.pages?.map((identities) =>
|
||||
identities.map(({ name, id }) => (
|
||||
identities.map(({ name, id, isInstanceAdmin }) => (
|
||||
<Tr key={`identity-${id}`} className="w-full">
|
||||
<Td>{name}</Td>
|
||||
<Td>
|
||||
{name}
|
||||
{isInstanceAdmin && (
|
||||
<Badge variant="primary" className="ml-2">
|
||||
Server Admin
|
||||
</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{isInstanceAdmin && (
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{isInstanceAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("removeServerAdmin", { name, id });
|
||||
}}
|
||||
>
|
||||
Remove Server Admin
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
))
|
||||
)}
|
||||
@@ -81,11 +133,49 @@ const IdentityPanelTable = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const IdentityPanel = () => (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||
export const IdentityPanel = () => {
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"removeServerAdmin"
|
||||
] as const);
|
||||
|
||||
const { mutate: deleteIdentitySuperAdminAccess } = useAdminRemoveIdentitySuperAdminAccess();
|
||||
|
||||
const handleRemoveServerAdmin = async () => {
|
||||
const { id } = popUp?.removeServerAdmin?.data as { id: string; name: string };
|
||||
|
||||
try {
|
||||
await deleteIdentitySuperAdminAccess(id);
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully removed server admin permissions"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Error removing server admin permissions"
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpClose("removeServerAdmin");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||
</div>
|
||||
<IdentityPanelTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeServerAdmin.isOpen}
|
||||
title={`Are you sure want to remove Server Admin permissions from ${
|
||||
(popUp?.removeServerAdmin?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
subTitle=""
|
||||
onChange={(isOpen) => handlePopUpToggle("removeServerAdmin", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleRemoveServerAdmin}
|
||||
buttonText="Remove Access"
|
||||
/>
|
||||
</div>
|
||||
<IdentityPanelTable />
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,22 +33,26 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useSubscription, useUser } from "@app/context";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { useDebounce, usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useAdminDeleteUser,
|
||||
useAdminGetUsers,
|
||||
useAdminGrantServerAdminAccess
|
||||
useAdminGrantServerAdminAccess,
|
||||
useRemoveUserServerAdminAccess
|
||||
} from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions";
|
||||
const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions from user";
|
||||
|
||||
const UserPanelTable = ({
|
||||
handlePopUpOpen
|
||||
}: {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["removeUser", "upgradePlan", "upgradeToServerAdmin"]>,
|
||||
popUpName: keyof UsePopUpState<
|
||||
["removeUser", "upgradePlan", "upgradeToServerAdmin", "removeServerAdmin"]
|
||||
>,
|
||||
data?: {
|
||||
username: string;
|
||||
id: string;
|
||||
@@ -58,8 +62,6 @@ const UserPanelTable = ({
|
||||
}) => {
|
||||
const [searchUserFilter, setSearchUserFilter] = useState("");
|
||||
const [adminsOnly, setAdminsOnly] = useState(false);
|
||||
const { user } = useUser();
|
||||
const userId = user?.id || "";
|
||||
const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
@@ -143,45 +145,61 @@ const UserPanelTable = ({
|
||||
</Td>
|
||||
<Td className="w-5/12">{email}</Td>
|
||||
<Td>
|
||||
{userId !== id && (
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("removeUser", { username, id });
|
||||
}}
|
||||
>
|
||||
Remove User
|
||||
</DropdownMenuItem>
|
||||
{!superAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("removeUser", { username, id });
|
||||
if (!subscription?.instanceUserManagement) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
username,
|
||||
id,
|
||||
message: addServerAdminUpgradePlanMessage
|
||||
});
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("upgradeToServerAdmin", { username, id });
|
||||
}}
|
||||
>
|
||||
Remove User
|
||||
Make User Server Admin
|
||||
</DropdownMenuItem>
|
||||
{!superAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!subscription?.instanceUserManagement) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
username,
|
||||
id,
|
||||
message: addServerAdminUpgradePlanMessage
|
||||
});
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("upgradeToServerAdmin", { username, id });
|
||||
}}
|
||||
>
|
||||
Make User Server Admin
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{superAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!subscription?.instanceUserManagement) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
username,
|
||||
id,
|
||||
message: removeServerAdminUpgradePlanMessage
|
||||
});
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("removeServerAdmin", { username, id });
|
||||
}}
|
||||
>
|
||||
Remove Server Admin
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
@@ -212,11 +230,13 @@ export const UserPanel = () => {
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"removeUser",
|
||||
"upgradePlan",
|
||||
"upgradeToServerAdmin"
|
||||
"upgradeToServerAdmin",
|
||||
"removeServerAdmin"
|
||||
] as const);
|
||||
|
||||
const { mutateAsync: deleteUser } = useAdminDeleteUser();
|
||||
const { mutateAsync: grantAdminAccess } = useAdminGrantServerAdminAccess();
|
||||
const { mutateAsync: removeAdminAccess } = useRemoveUserServerAdminAccess();
|
||||
|
||||
const handleRemoveUser = async () => {
|
||||
const { id } = popUp?.removeUser?.data as { id: string; username: string };
|
||||
@@ -256,6 +276,25 @@ export const UserPanel = () => {
|
||||
handlePopUpClose("upgradeToServerAdmin");
|
||||
};
|
||||
|
||||
const handleRemoveServerAdminAccess = async () => {
|
||||
const { id } = popUp?.removeServerAdmin?.data as { id: string; username: string };
|
||||
|
||||
try {
|
||||
await removeAdminAccess(id);
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully removed server admin access from user"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Error removing server admin access from user"
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpClose("removeServerAdmin");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4">
|
||||
@@ -282,6 +321,17 @@ export const UserPanel = () => {
|
||||
onDeleteApproved={handleGrantServerAdminAccess}
|
||||
buttonText="Grant Access"
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeServerAdmin.isOpen}
|
||||
title={`Are you sure want to remove Server Admin permissions from ${
|
||||
(popUp?.removeServerAdmin?.data as { id: string; username: string })?.username || ""
|
||||
}?`}
|
||||
subTitle=""
|
||||
onChange={(isOpen) => handlePopUpToggle("removeServerAdmin", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleRemoveServerAdminAccess}
|
||||
buttonText="Remove Access"
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
|
||||
+107
-90
@@ -46,12 +46,14 @@ const schema = z.object({
|
||||
caCert: z.string().trim().default(""),
|
||||
boundIssuer: z.string().min(1),
|
||||
boundAudiences: z.string().optional().default(""),
|
||||
boundClaims: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
),
|
||||
boundClaims: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
claimMetadataMapping: z
|
||||
.array(
|
||||
z.object({
|
||||
@@ -59,8 +61,7 @@ const schema = z.object({
|
||||
value: z.string()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.nullable(),
|
||||
.default([]),
|
||||
boundSubject: z.string().optional().default("")
|
||||
});
|
||||
|
||||
@@ -105,7 +106,9 @@ export const IdentityOidcAuthForm = ({
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
boundClaims: [],
|
||||
claimMetadataMapping: []
|
||||
}
|
||||
});
|
||||
const {
|
||||
@@ -172,7 +175,8 @@ export const IdentityOidcAuthForm = ({
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
claimMetadataMapping: []
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
@@ -253,7 +257,9 @@ export const IdentityOidcAuthForm = ({
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||
setTabValue(
|
||||
["accessTokenTrustedIps", "caCert", "boundClaims"].includes(Object.keys(fields)[0])
|
||||
["accessTokenTrustedIps", "caCert", "claimMetadataMapping"].includes(
|
||||
Object.keys(fields)[0]
|
||||
)
|
||||
? IdentityFormTab.Advanced
|
||||
: IdentityFormTab.Configuration
|
||||
);
|
||||
@@ -343,63 +349,6 @@ export const IdentityOidcAuthForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={IdentityFormTab.Advanced}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCert"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA Certificate"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{boundClaimsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
@@ -479,6 +428,65 @@ export const IdentityOidcAuthForm = ({
|
||||
Add Claims
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={IdentityFormTab.Advanced}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCert"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA Certificate"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{claimMetadataMappingFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
@@ -488,12 +496,23 @@ export const IdentityOidcAuthForm = ({
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Claim Metadata Mapping" : undefined}
|
||||
label={index === 0 ? "Token Claim Mapping" : undefined}
|
||||
icon={
|
||||
index === 0 ? (
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content="Map token claims to metadata. This can later be accessed in attribute based permission as identity.metadata.oidc.claims.<>."
|
||||
content={
|
||||
<div className="w-[180px]">
|
||||
<p>Map OIDC token claims to metadata fields</p>
|
||||
<p className="mt-2 text-sm">Example:</p>
|
||||
<p className="mt-1 text-sm">
|
||||
'role' → 'token.groups'
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
Becomes: identity.metadata.oidc.claims.role
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
@@ -505,7 +524,7 @@ export const IdentityOidcAuthForm = ({
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e)}
|
||||
placeholder="property"
|
||||
placeholder="Field name"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
@@ -524,7 +543,7 @@ export const IdentityOidcAuthForm = ({
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e)}
|
||||
placeholder="key1.nested-key2"
|
||||
placeholder="Token claim"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
@@ -554,9 +573,10 @@ export const IdentityOidcAuthForm = ({
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add Mapping
|
||||
Add Token Mapping
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
@@ -627,24 +647,21 @@ export const IdentityOidcAuthForm = ({
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isUpdate ? "Update" : "Add"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<div className="mt-8 flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
variant="outline_bg"
|
||||
className="mr-4"
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
{isUpdate ? "Update" : "Add"} Auth Method
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ export const AuditLogsPage = () => {
|
||||
title="Audit logs"
|
||||
description="Audit logs for security and compliance teams to monitor information access."
|
||||
/>
|
||||
<LogsSection filterClassName="static py-2" showFilters isOrgAuditLogs showActorColumn />
|
||||
<LogsSection filterClassName="static py-2" showFilters />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/en
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { LogsFilter } from "./LogsFilter";
|
||||
import { LogsTable, TAuditLogTableHeader } from "./LogsTable";
|
||||
import { LogsTable } from "./LogsTable";
|
||||
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
|
||||
|
||||
type Props = {
|
||||
@@ -25,22 +25,11 @@ type Props = {
|
||||
|
||||
showFilters?: boolean;
|
||||
filterClassName?: string;
|
||||
isOrgAuditLogs?: boolean;
|
||||
showActorColumn?: boolean;
|
||||
remappedHeaders?: Partial<Record<TAuditLogTableHeader, string>>;
|
||||
refetchInterval?: number;
|
||||
};
|
||||
|
||||
export const LogsSection = withPermission(
|
||||
({
|
||||
presets,
|
||||
filterClassName,
|
||||
remappedHeaders,
|
||||
isOrgAuditLogs,
|
||||
showActorColumn,
|
||||
refetchInterval,
|
||||
showFilters
|
||||
}: Props) => {
|
||||
({ presets, filterClassName, refetchInterval, showFilters }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
@@ -90,9 +79,6 @@ export const LogsSection = withPermission(
|
||||
)}
|
||||
<LogsTable
|
||||
refetchInterval={refetchInterval}
|
||||
remappedHeaders={remappedHeaders}
|
||||
isOrgAuditLogs={isOrgAuditLogs}
|
||||
showActorColumn={!!showActorColumn}
|
||||
filter={{
|
||||
secretPath: debouncedSecretPath || undefined,
|
||||
eventMetadata: presets?.eventMetadata,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Fragment } from "react";
|
||||
import { faFile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faFile, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useGetAuditLogs } from "@app/hooks/api";
|
||||
@@ -19,32 +21,13 @@ import { TGetAuditLogsFilter } from "@app/hooks/api/auditLogs/types";
|
||||
import { LogsTableRow } from "./LogsTableRow";
|
||||
|
||||
type Props = {
|
||||
isOrgAuditLogs?: boolean;
|
||||
showActorColumn: boolean;
|
||||
filter?: TGetAuditLogsFilter;
|
||||
remappedHeaders?: Partial<Record<TAuditLogTableHeader, string>>;
|
||||
refetchInterval?: number;
|
||||
};
|
||||
|
||||
const AUDIT_LOG_LIMIT = 15;
|
||||
|
||||
const TABLE_HEADERS = [
|
||||
"Timestamp (MM/DD/YYYY)",
|
||||
"Event",
|
||||
"Project",
|
||||
"Actor",
|
||||
"Source",
|
||||
"Metadata"
|
||||
] as const;
|
||||
export type TAuditLogTableHeader = (typeof TABLE_HEADERS)[number];
|
||||
|
||||
export const LogsTable = ({
|
||||
showActorColumn,
|
||||
isOrgAuditLogs,
|
||||
filter,
|
||||
remappedHeaders,
|
||||
refetchInterval
|
||||
}: Props) => {
|
||||
export const LogsTable = ({ filter, refetchInterval }: Props) => {
|
||||
// Determine the project ID for filtering
|
||||
const filterProjectId =
|
||||
// Use the projectId from the filter if it exists
|
||||
@@ -69,38 +52,37 @@ export const LogsTable = ({
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
{TABLE_HEADERS.map((header, idx) => {
|
||||
if (
|
||||
(header === "Project" && !isOrgAuditLogs) ||
|
||||
(header === "Actor" && !showActorColumn)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Th key={`table-header-${idx + 1}`}>{remappedHeaders?.[header] || header}</Th>
|
||||
);
|
||||
})}
|
||||
<Th className="w-24" />
|
||||
<Th className="w-64">
|
||||
Timestamp
|
||||
<Tooltip
|
||||
className="normal-case"
|
||||
content="Time displayed in your system's time zone."
|
||||
sideOffset={10}
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="ml-1" />
|
||||
</Tooltip>
|
||||
</Th>
|
||||
<Th>Event</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isPending &&
|
||||
data?.pages?.map((group, i) => (
|
||||
<Fragment key={`audit-log-fragment-${i + 1}`}>
|
||||
{group.map((auditLog) => (
|
||||
{group.map((auditLog, index) => (
|
||||
<LogsTableRow
|
||||
showActorColumn={showActorColumn}
|
||||
isOrgAuditLogs={isOrgAuditLogs}
|
||||
rowNumber={index + i * AUDIT_LOG_LIMIT + 1}
|
||||
auditLog={auditLog}
|
||||
key={`audit-log-${auditLog.id}`}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
{isPending && <TableSkeleton innerKey="logs-table" columns={5} key="logs" />}
|
||||
{isPending && <TableSkeleton innerKey="logs-table" columns={3} key="logs-loading" />}
|
||||
{isEmpty && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<Td colSpan={3}>
|
||||
<EmptyState title="No audit logs on file" icon={faFile} />
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
@@ -1,128 +1,76 @@
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { Td, Tooltip, 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 } from "@app/hooks/api/auditLogs/types";
|
||||
import { Td, Tr } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { AuditLog } from "@app/hooks/api/auditLogs/types";
|
||||
|
||||
type Props = {
|
||||
auditLog: AuditLog;
|
||||
isOrgAuditLogs?: boolean;
|
||||
showActorColumn: boolean;
|
||||
rowNumber: number;
|
||||
};
|
||||
|
||||
export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Props) => {
|
||||
const renderActor = (actor: Actor) => {
|
||||
if (!actor) {
|
||||
return <Td />;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
case ActorType.IDENTITY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`${actor.metadata.name}`}</p>
|
||||
<p>Machine Identity</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.PLATFORM:
|
||||
return (
|
||||
<Td>
|
||||
<p>Platform</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.KMIP_CLIENT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{actor.metadata.name}</p>
|
||||
<p>KMIP Client</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.UNKNOWN_USER:
|
||||
return (
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<p>Unknown User</p>
|
||||
<Tooltip content="This action was performed by a user who was not authenticated at the time.">
|
||||
<FontAwesomeIcon className="text-mineshaft-400" icon={faQuestionCircle} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</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 = `${month}-${day}-${year} at ${hours}:${minutes} ${period}`;
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
const renderSource = () => {
|
||||
const { event, actor } = auditLog;
|
||||
|
||||
if (event.type === EventType.INTEGRATION_SYNCED) {
|
||||
if (actor.type === ActorType.USER) {
|
||||
return (
|
||||
<Td>
|
||||
<p>Manually triggered by {actor.metadata.email}</p>
|
||||
</Td>
|
||||
);
|
||||
}
|
||||
|
||||
// Platform / automatic syncs
|
||||
return (
|
||||
<Td>
|
||||
<p>Automatically synced by Infisical</p>
|
||||
</Td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Td>
|
||||
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
|
||||
<p>{auditLog.ipAddress}</p>
|
||||
</Td>
|
||||
);
|
||||
};
|
||||
type TagProps = {
|
||||
label: string;
|
||||
value?: string;
|
||||
};
|
||||
const Tag = ({ label, value }: TagProps) => {
|
||||
if (!value) return null;
|
||||
|
||||
return (
|
||||
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
|
||||
<Td>{formatDate(auditLog.createdAt)}</Td>
|
||||
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
|
||||
{isOrgAuditLogs && <Td>{auditLog?.projectName ?? auditLog?.projectId ?? "N/A"}</Td>}
|
||||
{showActorColumn && renderActor(auditLog.actor)}
|
||||
{renderSource()}
|
||||
<Td className="max-w-xs break-all">{JSON.stringify(auditLog.event.metadata || {})}</Td>
|
||||
</Tr>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<div className="rounded bg-mineshaft-600 p-0.5 pl-1 font-mono">{label}:</div>
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LogsTableRow = ({ auditLog, rowNumber }: Props) => {
|
||||
const [isOpen, setIsOpen] = useToggle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className="h-10 cursor-pointer border-x-0 border-b border-t-0 hover:bg-mineshaft-700"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setIsOpen.toggle()}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setIsOpen.toggle();
|
||||
}}
|
||||
isHoverable
|
||||
>
|
||||
<Td className="flex items-center gap-2 pr-0 align-top">
|
||||
<FontAwesomeIcon icon={isOpen ? faCaretDown : faCaretRight} />
|
||||
{rowNumber}
|
||||
</Td>
|
||||
<Td className="align-top">
|
||||
{format(new Date(auditLog.createdAt), "MMM do yyyy, hh:mm a")}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
<Tag label="event" value={auditLog.event.type} />
|
||||
<Tag label="actor" value={auditLog.actor.type} />
|
||||
{auditLog.actor.type === ActorType.USER && (
|
||||
<Tag label="user_email" value={auditLog.actor.metadata.email} />
|
||||
)}
|
||||
{auditLog.actor.type === ActorType.IDENTITY && (
|
||||
<Tag label="identity_name" value={auditLog.actor.metadata.name} />
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
{isOpen && (
|
||||
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
|
||||
<Td colSpan={3} className="px-3 py-2">
|
||||
<div className="thin-scrollbar my-1 max-h-96 overflow-auto whitespace-pre-wrap rounded border border-mineshaft-600 bg-bunker-800 p-2 font-mono leading-6">
|
||||
{JSON.stringify(auditLog, null, 4)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
-1
@@ -45,7 +45,6 @@ export const UserAuditLogsSection = withPermission(
|
||||
presets={{
|
||||
actorId: orgMembership.user.id
|
||||
}}
|
||||
isOrgAuditLogs
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
-3
@@ -29,9 +29,6 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
|
||||
</div>
|
||||
<LogsSection
|
||||
refetchInterval={4000}
|
||||
remappedHeaders={{
|
||||
Metadata: "Sync Status"
|
||||
}}
|
||||
showFilters={false}
|
||||
presets={{
|
||||
eventMetadata: { integrationId: integration.id },
|
||||
|
||||
+9
@@ -192,6 +192,15 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (integration.integration === "windmill" && integration.url) {
|
||||
return (
|
||||
<div>
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Instance URL" />
|
||||
<div className="text-sm text-mineshaft-300">{integration.url}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -110,6 +110,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
|
||||
const secretsToDelete = Object.values(selectedEntries.secret).reduce(
|
||||
(accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => {
|
||||
const entry = secretRecord[env.slug];
|
||||
if (!entry) return accum;
|
||||
const canDeleteSecret = permission.can(
|
||||
ProjectPermissionSecretActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
|
||||
-3
@@ -35,9 +35,6 @@ export const SecretSyncAuditLogsSection = ({ secretSync }: Props) => {
|
||||
{subscription.auditLogs ? (
|
||||
<LogsSection
|
||||
refetchInterval={4000}
|
||||
remappedHeaders={{
|
||||
Metadata: "Sync Status"
|
||||
}}
|
||||
showFilters={false}
|
||||
presets={{
|
||||
eventMetadata: { syncId: secretSync.id },
|
||||
|
||||
+58
-2
@@ -3,31 +3,75 @@ import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
export const WindmillAuthorizePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||
const [apiUrl, setApiUrl] = useState<string | null>(null);
|
||||
const [apiUrlErrorText, setApiUrlErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const isLocalOrPrivateIpAddress = (url: string): boolean => {
|
||||
try {
|
||||
const validUrl = new URL(url);
|
||||
// Check for localhost
|
||||
if (validUrl.hostname === "localhost" || validUrl.hostname === "127.0.0.1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for 10.x.x.x
|
||||
if (validUrl.hostname.match(/^10\.\d+\.\d+\.\d+/)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for host.docker.internal
|
||||
if (validUrl.hostname === "host.docker.internal") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for 192.168.x.x
|
||||
if (validUrl.hostname.match(/^192\.168\.\d+\.\d+/)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
setApiKeyErrorText("");
|
||||
setApiUrlErrorText("");
|
||||
if (apiKey.length === 0) {
|
||||
setApiKeyErrorText("API Key cannot be blank");
|
||||
return;
|
||||
}
|
||||
|
||||
if (apiUrl) {
|
||||
if (!apiUrl.startsWith("http://") && !apiUrl.startsWith("https://")) {
|
||||
setApiUrlErrorText("API URL must start with http:// or https://");
|
||||
return;
|
||||
}
|
||||
if (isInfisicalCloud() && isLocalOrPrivateIpAddress(apiUrl)) {
|
||||
setApiUrlErrorText("Local IPs not allowed as URL");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const integrationAuth = await mutateAsync({
|
||||
workspaceId: currentWorkspace.id,
|
||||
integration: "windmill",
|
||||
accessToken: apiKey
|
||||
accessToken: apiKey,
|
||||
url: apiUrl ?? undefined
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -57,6 +101,18 @@ export const WindmillAuthorizePage = () => {
|
||||
>
|
||||
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Windmill Instance URL"
|
||||
errorText={apiUrlErrorText}
|
||||
isError={apiUrlErrorText !== ""}
|
||||
tooltipText="If you are using a custom domain, enter it here. Otherwise, leave it blank."
|
||||
>
|
||||
<Input
|
||||
value={apiUrl ?? ""}
|
||||
onChange={(e) => setApiUrl(e.target.value.trim() === "" ? null : e.target.value.trim())}
|
||||
placeholder="https://xxxx.windmill.dev"
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
|
||||
+2
-1
@@ -67,7 +67,8 @@ export const WindmillConfigurePage = () => {
|
||||
(integrationAuthApp) => integrationAuthApp.name === targetApp
|
||||
)?.appId,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
secretPath
|
||||
secretPath,
|
||||
url: integrationAuth.url ?? undefined
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
Reference in New Issue
Block a user