feat(secret-ref): implemented ui for service token changes

This commit is contained in:
akhilmhdh
2023-07-05 23:00:17 +05:30
parent 0cee453202
commit 7fe4089bb0
4 changed files with 406 additions and 376 deletions
@@ -1,9 +1,13 @@
export type ServiceTokenScope = {
environment: string;
secretPath: string;
};
export type ServiceToken = { export type ServiceToken = {
_id: string; _id: string;
name: string; name: string;
workspace: string; workspace: string;
environment: string; scopes: ServiceTokenScope[];
secretPath: string;
user: string; user: string;
expiresAt: string; expiresAt: string;
createdAt: string; createdAt: string;
@@ -14,9 +18,8 @@ export type ServiceToken = {
export type CreateServiceTokenDTO = { export type CreateServiceTokenDTO = {
name: string; name: string;
workspaceId: string; workspaceId: string;
environment: string; scopes: ServiceTokenScope[];
expiresIn: number; expiresIn: number;
secretPath: string;
encryptedKey: string; encryptedKey: string;
iv: string; iv: string;
tag: string; tag: string;
@@ -1,9 +1,9 @@
import crypto from "crypto"; import crypto from "crypto";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faCopy, faPlus, faTrashCan } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
@@ -27,10 +27,7 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { import { useCreateServiceToken, useGetUserWsKey } from "@app/hooks/api";
useCreateServiceToken,
useGetUserWsKey
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const apiTokenExpiry = [ const apiTokenExpiry = [
@@ -44,8 +41,16 @@ const apiTokenExpiry = [
const schema = yup.object({ const schema = yup.object({
name: yup.string().max(100).required().label("Service Token Name"), name: yup.string().max(100).required().label("Service Token Name"),
scopes: yup
.array(
yup.object({
environment: yup.string().max(50).required().label("Environment"), environment: yup.string().max(50).required().label("Environment"),
secretPath: yup.string().required().default("/").label("Secret Path"), secretPath: yup.string().required().default("/").label("Secret Path")
})
)
.min(1)
.required()
.label("Scope"),
expiresIn: yup.string().optional().label("Service Token Expiration"), expiresIn: yup.string().optional().label("Service Token Expiration"),
permissions: yup permissions: yup
.object() .object()
@@ -64,10 +69,7 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["createAPIToken"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["createAPIToken"]>, state?: boolean) => void;
}; };
export const AddServiceTokenModal = ({ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
popUp,
handlePopUpToggle
}: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
@@ -77,9 +79,14 @@ export const AddServiceTokenModal = ({
handleSubmit, handleSubmit,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: yupResolver(schema) resolver: yupResolver(schema),
defaultValues: {
scopes: [{ secretPath: "/", environment: currentWorkspace?.environments?.[0]?.slug }]
}
}); });
const { fields: tokenScopes, append, remove } = useFieldArray({ control, name: "scopes" });
const [newToken, setToken] = useState(""); const [newToken, setToken] = useState("");
const [isTokenCopied, setIsTokenCopied] = useToggle(false); const [isTokenCopied, setIsTokenCopied] = useToggle(false);
@@ -101,13 +108,7 @@ export const AddServiceTokenModal = ({
setIsTokenCopied.on(); setIsTokenCopied.on();
}; };
const onFormSubmit = async ({ const onFormSubmit = async ({ name, scopes, expiresIn, permissions }: FormData) => {
name,
environment,
secretPath,
expiresIn,
permissions
}: FormData) => {
try { try {
if (!currentWorkspace?._id) return; if (!currentWorkspace?._id) return;
if (!latestFileKey) return; if (!latestFileKey) return;
@@ -130,8 +131,7 @@ export const AddServiceTokenModal = ({
encryptedKey: ciphertext, encryptedKey: ciphertext,
iv, iv,
tag, tag,
environment, scopes,
secretPath,
expiresIn: Number(expiresIn), expiresIn: Number(expiresIn),
name, name,
workspaceId: currentWorkspace._id, workspaceId: currentWorkspace._id,
@@ -142,12 +142,10 @@ export const AddServiceTokenModal = ({
}); });
setToken(serviceToken); setToken(serviceToken);
createNotification({ createNotification({
text: "Successfully created a service token", text: "Successfully created a service token",
type: "success" type: "success"
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
createNotification({ createNotification({
@@ -190,13 +188,16 @@ export const AddServiceTokenModal = ({
</FormControl> </FormControl>
)} )}
/> />
{tokenScopes.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller <Controller
control={control} control={control}
name="environment" name={`scopes.${index}.environment`}
defaultValue={currentWorkspace?.environments?.[0]?.slug} defaultValue={currentWorkspace?.environments?.[0]?.slug}
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="Environment" className="mb-0"
label={index === 0 ? "Environment" : undefined}
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
> >
@@ -217,29 +218,50 @@ export const AddServiceTokenModal = ({
/> />
<Controller <Controller
control={control} control={control}
name="secretPath" name={`scopes.${index}.secretPath`}
defaultValue="/" defaultValue="/"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Secrets Path" className="mb-0 flex-grow"
label={index === 0 ? "Secrets Path" : undefined}
isError={Boolean(error)} isError={Boolean(error)}
helperText="Tokens can be scoped to a folder path. Default path is /"
errorText={error?.message} errorText={error?.message}
> >
<Input {...field} placeholder="Provide a path, default is /" /> <Input {...field} placeholder="can be /, /nested/**, /**/deep" />
</FormControl> </FormControl>
)} )}
/> />
<IconButton
className="p-3"
ariaLabel="remove"
colorSchema="danger"
onClick={() => remove(index)}
>
<FontAwesomeIcon icon={faTrashCan} size="sm" />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() =>
append({
environment: currentWorkspace?.environments?.[0]?.slug || "",
secretPath: ""
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add Scope
</Button>
</div>
<Controller <Controller
control={control} control={control}
name="expiresIn" name="expiresIn"
defaultValue={String(apiTokenExpiry?.[0]?.value)} defaultValue={String(apiTokenExpiry?.[0]?.value)}
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl label="Expiration" errorText={error?.message} isError={Boolean(error)}>
label="Expiration"
errorText={error?.message}
isError={Boolean(error)}
>
<Select <Select
defaultValue={field.value} defaultValue={field.value}
{...field} {...field}
@@ -340,4 +362,4 @@ export const AddServiceTokenModal = ({
</ModalContent> </ModalContent>
</Modal> </Modal>
); );
} };
@@ -3,14 +3,9 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import { Button, DeleteActionModal } from "@app/components/v2";
Button,
DeleteActionModal,
} from "@app/components/v2";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { import { useDeleteServiceToken } from "@app/hooks/api";
useDeleteServiceToken
} from "@app/hooks/api";
import { AddServiceTokenModal } from "./AddServiceTokenModal"; import { AddServiceTokenModal } from "./AddServiceTokenModal";
import { ServiceTokenTable } from "./ServiceTokenTable"; import { ServiceTokenTable } from "./ServiceTokenTable";
@@ -29,7 +24,9 @@ export const ServiceTokenSection = () => {
const onDeleteApproved = async () => { const onDeleteApproved = async () => {
try { try {
deleteServiceToken.mutateAsync((popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id); deleteServiceToken.mutateAsync(
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id
);
createNotification({ createNotification({
text: "Successfully deleted service token", text: "Successfully deleted service token",
type: "success" type: "success"
@@ -46,9 +43,11 @@ export const ServiceTokenSection = () => {
}; };
return ( return (
<div className="mb-6 p-4 bg-mineshaft-900 max-w-screen-lg rounded-lg border border-mineshaft-600"> <div className="mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between mb-8"> <div className="mb-2 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">{t("section.token.service-tokens")}</p> <p className="text-xl font-semibold text-mineshaft-100">
{t("section.token.service-tokens")}
</p>
<Button <Button
colorSchema="secondary" colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
@@ -59,19 +58,14 @@ export const ServiceTokenSection = () => {
Create token Create token
</Button> </Button>
</div> </div>
<p className="text-gray-400 mb-8">{t("section.token.service-tokens-description")}</p> <p className="mb-8 text-gray-400">{t("section.token.service-tokens-description")}</p>
<ServiceTokenTable <ServiceTokenTable handlePopUpOpen={handlePopUpOpen} />
handlePopUpOpen={handlePopUpOpen} <AddServiceTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
/>
<AddServiceTokenModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteAPITokenConfirmation.isOpen} isOpen={popUp.deleteAPITokenConfirmation.isOpen}
title={ title={`Delete ${
`Delete ${(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || " "} service token?` (popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || " "
} } service token?`}
onChange={(isOpen) => handlePopUpToggle("deleteAPITokenConfirmation", isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteAPITokenConfirmation", isOpen)}
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name} deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
onClose={() => handlePopUpClose("deleteAPITokenConfirmation")} onClose={() => handlePopUpClose("deleteAPITokenConfirmation")}
@@ -1,4 +1,4 @@
import { faKey, faTrashCan } from "@fortawesome/free-solid-svg-icons"; import { faFolder, faKey, faTrashCan } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
@@ -30,9 +30,7 @@ type Props = {
) => void; ) => void;
}; };
export const ServiceTokenTable = ({ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
handlePopUpOpen
}: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetUserWsServiceTokens({ const { data, isLoading } = useGetUserWsServiceTokens({
workspaceID: currentWorkspace?._id || "" workspaceID: currentWorkspace?._id || ""
@@ -44,21 +42,34 @@ export const ServiceTokenTable = ({
<THead> <THead>
<Tr> <Tr>
<Th>Token Name</Th> <Th>Token Name</Th>
<Th>Environment</Th> <Th>Envrionment - Secret Path</Th>
<Th>Secret Path</Th>
<Th>Valid Until</Th> <Th>Valid Until</Th>
<Th aria-label="button" /> <Th aria-label="button" />
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={4} key="project-service-tokens" />} {isLoading && <TableSkeleton columns={4} key="project-service-tokens" />}
{!isLoading && data && data.map((row) => ( {!isLoading &&
data &&
data.map((row) => (
<Tr key={row._id}> <Tr key={row._id}>
<Td>{row.name}</Td> <Td>{row.name}</Td>
<Td>{row.environment}</Td> <Td>
<Td>{row.secretPath}</Td> <div className="mb-2 flex flex-col flex-wrap space-y-1">
{row?.scopes.map(({ secretPath, environment }) => (
<div
key={`${row._id}-${environment}-${secretPath}`}
className="inline-flex items-center space-x-1 rounded-md border border-mineshaft-600 p-1 px-2"
>
<div className="mr-2 border-r border-mineshaft-600 pr-2">{environment}</div>
<FontAwesomeIcon icon={faFolder} size="sm" />
<span className="pl-2">{secretPath}</span>
</div>
))}
</div>
</Td>
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td> <Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
<Td className="flex items-center justify-end"> <Td>
<IconButton <IconButton
onClick={() => onClick={() =>
handlePopUpOpen("deleteAPITokenConfirmation", { handlePopUpOpen("deleteAPITokenConfirmation", {
@@ -85,4 +96,4 @@ export const ServiceTokenTable = ({
</Table> </Table>
</TableContainer> </TableContainer>
); );
} };