Continue ST V3 roles

This commit is contained in:
Tuan Dang
2023-11-23 11:56:56 +07:00
parent c37ff79927
commit 597e1e1ca8
49 changed files with 1104 additions and 390 deletions
@@ -1,41 +1,32 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { useGetRoles } from "@app/hooks/api";
import { TRole } from "@app/hooks/api/roles/types";
import { OrgMembersTable } from "./components/OrgMembersTable";
import { OrgRoleTabSection } from "./components/OrgRoleTabSection";
import { OrgServiceTokenTab } from "./components/OrgServiceTokenTab";
enum TabSections {
Member = "members",
Roles = "roles"
Roles = "roles",
ServiceTokens = "service-tokens"
}
export const MembersPage = withPermission(
() => {
const { t } = useTranslation();
const { currentOrg } = useOrganization();
const orgId = currentOrg?._id || "";
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
orgId
});
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mb-6 w-full py-6 px-6 max-w-7xl mx-auto">
<p className="mr-4 mb-4 text-3xl font-semibold text-white">
{t("section.members.org-members")}
Access Control
</p>
<Tabs defaultValue={TabSections.Member}>
<TabList>
<Tab value={TabSections.Member}>Members</Tab>
<Tab value={TabSections.ServiceTokens}>Service Accounts</Tab>
<Tab value={TabSections.Roles}>Roles</Tab>
</TabList>
<TabPanel value={TabSections.Member}>
@@ -46,17 +37,14 @@ export const MembersPage = withPermission(
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgMembersTable
roles={roles as TRole<undefined>[]}
isRolesLoading={isRolesLoading}
/>
<OrgMembersTable />
</motion.div>
</TabPanel>
<TabPanel value={TabSections.ServiceTokens}>
<OrgServiceTokenTab />
</TabPanel>
<TabPanel value={TabSections.Roles}>
<OrgRoleTabSection
roles={roles as TRole<undefined>[]}
isRolesLoading={isRolesLoading}
/>
<OrgRoleTabSection />
</TabPanel>
</Tabs>
</div>
@@ -55,27 +55,21 @@ import {
useAddUserToOrg,
useDeleteOrgMembership,
useGetOrgUsers,
useGetRoles,
useGetSSOConfig,
useGetUserWorkspaceMemberships,
useGetUserWsKey,
useUpdateOrgUserRole,
useUploadWsKey
} from "@app/hooks/api";
import { TRole } from "@app/hooks/api/roles/types";
useUploadWsKey} from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
type Props = {
roles?: TRole<undefined>[];
isRolesLoading?: boolean;
};
const addMemberFormSchema = yup.object({
email: yup.string().email().required().label("Email").trim().lowercase()
});
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
export const OrgMembersTable = ({ roles = [], isRolesLoading }: Props) => {
export const OrgMembersTable = () => {
const router = useRouter();
const { createNotification } = useNotificationContext();
@@ -86,6 +80,10 @@ export const OrgMembersTable = ({ roles = [], isRolesLoading }: Props) => {
const orgId = currentOrg?._id || "";
const workspaceId = currentWorkspace?._id || "";
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
orgId
});
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(orgId);
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const { data: serverDetails } = useFetchServerStatus();
@@ -211,7 +209,7 @@ export const OrgMembersTable = ({ roles = [], isRolesLoading }: Props) => {
const findRoleFromId = useCallback(
(roleId: string) => {
return roles.find(({ _id: id }) => id === roleId);
return (roles || []).find(({ _id: id }) => id === roleId);
},
[roles]
);
@@ -377,7 +375,7 @@ export const OrgMembersTable = ({ roles = [], isRolesLoading }: Props) => {
onRoleChange(orgMembershipId, selectedRole)
}
>
{roles
{(roles || [])
.filter(({ slug }) =>
slug === "owner" ? isIamOwner || role === "owner" : true
)
@@ -6,14 +6,8 @@ import { TRole } from "@app/hooks/api/roles/types";
import { OrgRoleModifySection } from "./OrgRoleModifySection";
import { OrgRoleTable } from "./OrgRoleTable";
type Props = {
roles?: TRole<undefined>[];
isRolesLoading?: boolean;
};
export const OrgRoleTabSection = ({ roles = [], isRolesLoading }: Props) => {
export const OrgRoleTabSection = () => {
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["editRole"] as const);
return popUp.editRole.isOpen ? (
<motion.div
key="role-modify"
@@ -35,11 +29,7 @@ export const OrgRoleTabSection = ({ roles = [], isRolesLoading }: Props) => {
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<OrgRoleTable
roles={roles}
isRolesLoading={isRolesLoading}
onSelectRole={(role) => handlePopUpOpen("editRole", role)}
/>
<OrgRoleTable onSelectRole={(role) => handlePopUpOpen("editRole", role)} />
</motion.div>
);
};
@@ -20,22 +20,26 @@ import {
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteRole } from "@app/hooks/api";
import { useDeleteRole, useGetRoles } from "@app/hooks/api";
import { TRole } from "@app/hooks/api/roles/types";
type Props = {
isRolesLoading?: boolean;
roles?: TRole<undefined>[];
onSelectRole: (role?: TRole<undefined>) => void;
};
export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props) => {
export const OrgRoleTable = ({
onSelectRole
}: Props) => {
const [searchRoles, setSearchRoles] = useState("");
const { currentOrg } = useOrganization();
const orgId = currentOrg?._id || "";
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
orgId
});
const { mutateAsync: deleteRole } = useDeleteRole();
const handleRoleDelete = async () => {
@@ -88,7 +92,7 @@ export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props
</THead>
<TBody>
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
{roles?.map((role) => {
{(roles as TRole<undefined>[])?.map((role) => {
const { _id: id, name, slug } = role;
const isNonMutatable = ["owner", "admin", "member"].includes(slug);
@@ -0,0 +1,17 @@
import { motion } from "framer-motion";
import { ServiceTokenV3Section } from "./components";
export const OrgServiceTokenTab = () => {
return (
<motion.div
key="panel-service-token"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<ServiceTokenV3Section />
</motion.div>
);
}
@@ -0,0 +1,488 @@
import { useEffect, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faCheck, faCopy,faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import { motion } from "framer-motion";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Select,
SelectItem,
Switch,
Tab,
TabList,
TabPanel,
Tabs,
UpgradePlanModal} from "@app/components/v2";
import {
useOrganization,
useSubscription
} from "@app/context";
import { useToggle } from "@app/hooks";
import {
useCreateServiceTokenV3,
useGetRoles,
useUpdateServiceTokenV3
} from "@app/hooks/api";
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
enum TabSections {
General = "general",
Advanced = "advanced"
}
const expirations = [
{ label: "Never", value: "" },
{ label: "1 day", value: "86400" },
{ label: "7 days", value: "604800" },
{ label: "1 month", value: "2592000" },
{ label: "6 months", value: "15552000" },
{ label: "12 months", value: "31104000" }
];
const schema = yup.object({
name: yup.string().required("ST V3 name is required"),
expiresIn: yup.string(),
accessTokenTTL: yup
.string()
.test("is-positive-integer", "Access Token TTL must be a positive integer", (value) => {
if (typeof value === "undefined") {
return false;
}
const num = parseInt(value, 10);
return !Number.isNaN(num) && num > 0 && String(num) === value;
})
.required("Access Token TTL is required"),
role: yup.string().required("ST V3 role is required"),
trustedIps: yup
.array(
yup.object({
ipAddress: yup.string().max(50).required().label("IP Address")
})
)
.min(1)
.required()
.label("Trusted IP"),
isRefreshTokenRotationEnabled: yup.boolean().default(false)
}).required();
export type FormData = yup.InferType<typeof schema>;
type Props = {
popUp: UsePopUpState<["serviceTokenV3", "upgradePlan"]>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["serviceTokenV3", "upgradePlan"]>, state?: boolean) => void;
};
export const AddServiceTokenV3Modal = ({
popUp,
handlePopUpOpen,
handlePopUpToggle
}: Props) => {
const [newServiceTokenJSON, setNewServiceTokenJSON] = useState("");
const [isServiceTokenJSONCopied, setIsServiceTokenJSONCopied] = useToggle(false);
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const orgId = currentOrg?._id || "";
const { data: roles } = useGetRoles({
orgId
});
const { mutateAsync: createMutateAsync } = useCreateServiceTokenV3();
const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3();
const { createNotification } = useNotificationContext();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
name: "",
accessTokenTTL: "7200",
trustedIps: [{
ipAddress: "0.0.0.0/0"
}]
}
});
useEffect(() => {
let timer: NodeJS.Timeout;
if (isServiceTokenJSONCopied) {
timer = setTimeout(() => setIsServiceTokenJSONCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [setIsServiceTokenJSONCopied]);
const copyTokenToClipboard = () => {
navigator.clipboard.writeText(newServiceTokenJSON);
setIsServiceTokenJSONCopied.on();
};
useEffect(() => {
const serviceTokenData = popUp?.serviceTokenV3?.data as {
serviceTokenDataId: string;
name: string;
role: string;
customRole: {
name: string;
slug: string;
};
trustedIps: ServiceTokenV3TrustedIp[];
accessTokenTTL: number;
isRefreshTokenRotationEnabled: boolean;
};
if (!roles?.length) return;
if (serviceTokenData) {
reset({
name: serviceTokenData.name,
role: serviceTokenData?.customRole?.slug ?? serviceTokenData.role,
trustedIps: serviceTokenData.trustedIps.map(({
ipAddress,
prefix
}: ServiceTokenV3TrustedIp) => {
return ({
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
});
}),
accessTokenTTL: String(serviceTokenData.accessTokenTTL),
isRefreshTokenRotationEnabled: serviceTokenData.isRefreshTokenRotationEnabled
});
} else {
reset({
name: "",
accessTokenTTL: "7200",
role: roles[0].slug,
trustedIps: [{
ipAddress: "0.0.0.0/0"
}]
});
}
}, [popUp?.serviceTokenV3?.data, roles]);
const { fields: tokenTrustedIps, append: appendTrustedIp, remove: removeTrustedIp } = useFieldArray({ control, name: "trustedIps" });
const onFormSubmit = async ({
name,
expiresIn,
accessTokenTTL,
role,
trustedIps,
isRefreshTokenRotationEnabled
}: FormData) => {
try {
const serviceTokenData = popUp?.serviceTokenV3?.data as {
serviceTokenDataId: string;
name: string;
role: string;
};
if (serviceTokenData) {
// update
await updateMutateAsync({
serviceTokenDataId: serviceTokenData.serviceTokenDataId,
name,
role,
trustedIps,
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
accessTokenTTL: Number(accessTokenTTL),
isRefreshTokenRotationEnabled
});
handlePopUpToggle("serviceTokenV3", false);
} else {
const { refreshToken } = await createMutateAsync({
name,
role,
organizationId: orgId,
trustedIps,
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
accessTokenTTL: Number(accessTokenTTL),
isRefreshTokenRotationEnabled
});
setNewServiceTokenJSON(refreshToken);
}
createNotification({
text: `Successfully ${popUp?.serviceTokenV3?.data ? "updated" : "created"} service account`,
type: "success"
});
reset();
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${popUp?.serviceTokenV3?.data ? "updated" : "created"} service account`,
type: "error"
});
}
}
const hasServiceTokenJSON = Boolean(newServiceTokenJSON);
return (
<Modal
isOpen={popUp?.serviceTokenV3?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("serviceTokenV3", isOpen);
reset();
setNewServiceTokenJSON("");
}}
>
<ModalContent title={`${popUp?.serviceTokenV3?.data ? "Update" : "Create"} Service Account`}>
{!hasServiceTokenJSON ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Tabs defaultValue={TabSections.General}>
<TabList>
<div className="flex flex-row border-b border-mineshaft-600 w-full">
<Tab value={TabSections.General}>General</Tab>
<Tab value={TabSections.Advanced}>Advanced</Tab>
</div>
</TabList>
<TabPanel value={TabSections.General}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="My ST V3"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="role"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.serviceTokenV3?.data ? "Update" : ""} Role`}
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(roles || []).map(({ name, slug }) => (
<SelectItem value={slug} key={`st-role-${slug}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="expiresIn"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.serviceTokenV3?.data ? "Update" : ""} Refresh Token Expires In`}
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{expirations.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={`api-key-expiration-${label}`}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Advanced}>
<div>
{tokenTrustedIps.map(({ id }, index) => (
<div className="flex items-end space-x-2 mb-3" key={id}>
<Controller
control={control}
name={`trustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Trusted IP" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendTrustedIp({
ipAddress: "0.0.0.0/0"
})
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<Controller
control={control}
defaultValue="7200"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="7200"
/>
</FormControl>
)}
/>
<div className="mt-8">
<Controller
control={control}
name="isRefreshTokenRotationEnabled"
render={({ field: { onChange, value } }) => (
<Switch
id="label-refresh-token-rotation"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
>
Refresh Token Rotation
</Switch>
)}
/>
<p className="mt-4 text-sm font-normal text-mineshaft-400">When enabled, as a result of exchanging a refresh token, a new refresh token will be issued and the existing token will be invalidated.</p>
</div>
</div>
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.serviceTokenV3?.data ? "Update" : "Create"}
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
) : (
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{newServiceTokenJSON}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isServiceTokenJSONCopied ? 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">
Click to copy
</span>
</IconButton>
</div>
)}
<UpgradePlanModal
isOpen={popUp?.upgradePlan?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
/>
</ModalContent>
</Modal>
);
}
@@ -0,0 +1,98 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import {
useDeleteServiceTokenV3
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { AddServiceTokenV3Modal } from "./AddServiceTokenV3Modal";
import { ServiceTokenV3Table } from "./ServiceTokenV3Table";
export const ServiceTokenV3Section = withPermission(
() => {
const { createNotification } = useNotificationContext();
const { mutateAsync: deleteMutateAsync } = useDeleteServiceTokenV3();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"serviceTokenV3",
"deleteServiceTokenV3",
"upgradePlan"
] as const);
const onDeleteServiceTokenDataSubmit = async (serviceTokenDataId: string) => {
try {
await deleteMutateAsync({
serviceTokenDataId
});
createNotification({
text: "Successfully deleted service token v3",
type: "success"
});
handlePopUpClose("deleteServiceTokenV3");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete service token v3",
type: "error"
});
}
}
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between mb-8">
<p className="text-xl font-semibold text-mineshaft-100">
Service Accounts (Beta)
</p>
<OrgPermissionCan
I={OrgPermissionActions.Create}
a={OrgPermissionSubjects.ServiceTokens}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("serviceTokenV3")}
isDisabled={!isAllowed}
>
Create account
</Button>
)}
</OrgPermissionCan>
</div>
<ServiceTokenV3Table
handlePopUpOpen={handlePopUpOpen}
/>
<AddServiceTokenV3Modal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteServiceTokenV3.isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteServiceTokenV3?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteServiceTokenV3", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteServiceTokenDataSubmit(
(popUp?.deleteServiceTokenV3?.data as { serviceTokenDataId: string })?.serviceTokenDataId
)
}
/>
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.ServiceTokens }
);
@@ -0,0 +1,222 @@
import { faKey, faPencil,faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
Switch,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization} from "@app/context";
import {
useGetOrgServiceMemberships,
useUpdateServiceTokenV3} from "@app/hooks/api";
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteServiceTokenV3", "serviceTokenV3"]>,
data?: {
serviceTokenDataId?: string;
name?: string;
role?: string;
customRole?: {
name: string;
slug: string;
};
trustedIps?: ServiceTokenV3TrustedIp[];
accessTokenTTL?: number;
isRefreshTokenRotationEnabled?: boolean;
}
) => void;
};
export const ServiceTokenV3Table = ({
handlePopUpOpen
}: Props) => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3();
const { data, isLoading } = useGetOrgServiceMemberships(currentOrg?._id || "");
const handleToggleServiceTokenDataStatus = async ({
serviceTokenDataId,
isActive
}: {
serviceTokenDataId: string;
isActive: boolean;
}) => {
try {
await updateMutateAsync({
serviceTokenDataId,
isActive
});
createNotification({
text: `Successfully ${isActive ? "enabled" : "disabled"} service token v3`,
type: "success"
});
} catch (err) {
console.log(err);
createNotification({
text: `Failed to ${isActive ? "enable" : "disable"} service token v3`,
type: "error"
});
}
}
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Status</Th>
<Th>Role</Th>
{/* <Th>Trusted IPs</Th> */}
{/* <Th>Access Token TTL</Th> */}
{/* <Th>Created At</Th> */}
<Th>Valid Until</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={7} innerKey="service-tokens" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({
service: {
_id,
name,
isActive,
trustedIps,
// createdAt,
expiresAt,
accessTokenTTL,
isRefreshTokenRotationEnabled
},
role,
customRole
}) => {
return (
<Tr className="h-10" key={`st-v3-${_id}`}>
<Td>{name}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ServiceTokens}
>
{(isAllowed) => (
<Switch
id={`enable-service-token-${_id}`}
onCheckedChange={(value) => handleToggleServiceTokenDataStatus({
serviceTokenDataId: _id,
isActive: value
})}
isChecked={isActive}
isDisabled={!isAllowed}
>
<p className="w-12 mr-4">{isActive ? "Active" : "Inactive"}</p>
</Switch>
)}
</OrgPermissionCan>
</Td>
<Td>{customRole?.slug ?? role}</Td>
{/* <Td>
{trustedIps.map(({
_id: trustedIpId,
ipAddress,
prefix
}) => {
return (
<p key={`service-token-${_id}-}-trusted-ip-${trustedIpId}`}>
{`${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`}
</p>
);
})}
</Td> */}
{/* <Td>{accessTokenTTL}</Td> */}
{/* <Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td> */}
<Td>{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"}</Td>
<Td className="flex justify-end">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ServiceTokens}
>
{(isAllowed) => (
<IconButton
onClick={async () => {
handlePopUpOpen("serviceTokenV3", {
serviceTokenDataId: _id,
name,
role,
customRole,
trustedIps,
accessTokenTTL,
isRefreshTokenRotationEnabled
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.ServiceTokens}
>
{(isAllowed) => (
<IconButton
onClick={() => {
handlePopUpOpen("deleteServiceTokenV3", {
serviceTokenDataId: _id,
name
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</OrgPermissionCan>
</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={7}>
<EmptyState title="No service token v3 on file" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
@@ -0,0 +1 @@
export { ServiceTokenV3Section } from "./ServiceTokenV3Section";
@@ -0,0 +1 @@
export { ServiceTokenV3Section } from "./ServiceTokenV3Section";
@@ -0,0 +1 @@
export { OrgServiceTokenTab } from "./OrgServiceTokenTab";