mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 11:27:07 +00:00
feat(secret-ref): implemented ui for service token changes
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
+298
-276
@@ -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"),
|
||||||
environment: yup.string().max(50).required().label("Environment"),
|
scopes: yup
|
||||||
secretPath: yup.string().required().default("/").label("Secret Path"),
|
.array(
|
||||||
|
yup.object({
|
||||||
|
environment: yup.string().max(50).required().label("Environment"),
|
||||||
|
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()
|
||||||
@@ -60,284 +65,301 @@ const schema = yup.object({
|
|||||||
export type FormData = yup.InferType<typeof schema>;
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["createAPIToken"]>;
|
popUp: UsePopUpState<["createAPIToken"]>;
|
||||||
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,
|
const { t } = useTranslation();
|
||||||
handlePopUpToggle
|
const { createNotification } = useNotificationContext();
|
||||||
}: Props) => {
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { t } = useTranslation();
|
const {
|
||||||
const { createNotification } = useNotificationContext();
|
control,
|
||||||
const { currentWorkspace } = useWorkspace();
|
reset,
|
||||||
const {
|
handleSubmit,
|
||||||
control,
|
formState: { isSubmitting }
|
||||||
reset,
|
} = useForm<FormData>({
|
||||||
handleSubmit,
|
resolver: yupResolver(schema),
|
||||||
formState: { isSubmitting }
|
defaultValues: {
|
||||||
} = useForm<FormData>({
|
scopes: [{ secretPath: "/", environment: currentWorkspace?.environments?.[0]?.slug }]
|
||||||
resolver: yupResolver(schema)
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [newToken, setToken] = useState("");
|
const { fields: tokenScopes, append, remove } = useFieldArray({ control, name: "scopes" });
|
||||||
const [isTokenCopied, setIsTokenCopied] = useToggle(false);
|
|
||||||
|
|
||||||
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
const [newToken, setToken] = useState("");
|
||||||
const createServiceToken = useCreateServiceToken();
|
const [isTokenCopied, setIsTokenCopied] = useToggle(false);
|
||||||
const hasServiceToken = Boolean(newToken);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||||
let timer: NodeJS.Timeout;
|
const createServiceToken = useCreateServiceToken();
|
||||||
if (isTokenCopied) {
|
const hasServiceToken = Boolean(newToken);
|
||||||
timer = setTimeout(() => setIsTokenCopied.off(), 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
useEffect(() => {
|
||||||
}, [isTokenCopied]);
|
let timer: NodeJS.Timeout;
|
||||||
|
if (isTokenCopied) {
|
||||||
|
timer = setTimeout(() => setIsTokenCopied.off(), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
const copyTokenToClipboard = () => {
|
return () => clearTimeout(timer);
|
||||||
navigator.clipboard.writeText(newToken);
|
}, [isTokenCopied]);
|
||||||
setIsTokenCopied.on();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
const copyTokenToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(newToken);
|
||||||
|
setIsTokenCopied.on();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ name, scopes, expiresIn, permissions }: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!currentWorkspace?._id) return;
|
||||||
|
if (!latestFileKey) return;
|
||||||
|
|
||||||
|
const key = decryptAssymmetric({
|
||||||
|
ciphertext: latestFileKey.encryptedKey,
|
||||||
|
nonce: latestFileKey.nonce,
|
||||||
|
publicKey: latestFileKey.sender.publicKey,
|
||||||
|
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
||||||
|
});
|
||||||
|
|
||||||
|
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||||
|
|
||||||
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
|
plaintext: key,
|
||||||
|
key: randomBytes
|
||||||
|
});
|
||||||
|
|
||||||
|
const { serviceToken } = await createServiceToken.mutateAsync({
|
||||||
|
encryptedKey: ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
scopes,
|
||||||
|
expiresIn: Number(expiresIn),
|
||||||
name,
|
name,
|
||||||
environment,
|
workspaceId: currentWorkspace._id,
|
||||||
secretPath,
|
randomBytes,
|
||||||
expiresIn,
|
permissions: Object.entries(permissions)
|
||||||
permissions
|
.filter(([, permissionsValue]) => permissionsValue)
|
||||||
}: FormData) => {
|
.map(([permissionsKey]) => permissionsKey)
|
||||||
try {
|
});
|
||||||
if (!currentWorkspace?._id) return;
|
|
||||||
if (!latestFileKey) return;
|
|
||||||
|
|
||||||
const key = decryptAssymmetric({
|
setToken(serviceToken);
|
||||||
ciphertext: latestFileKey.encryptedKey,
|
createNotification({
|
||||||
nonce: latestFileKey.nonce,
|
text: "Successfully created a service token",
|
||||||
publicKey: latestFileKey.sender.publicKey,
|
type: "success"
|
||||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
});
|
||||||
});
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create a service token",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
return (
|
||||||
|
<Modal
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
isOpen={popUp?.createAPIToken?.isOpen}
|
||||||
plaintext: key,
|
onOpenChange={(open) => {
|
||||||
key: randomBytes
|
handlePopUpToggle("createAPIToken", open);
|
||||||
});
|
reset();
|
||||||
|
setToken("");
|
||||||
const { serviceToken } = await createServiceToken.mutateAsync({
|
}}
|
||||||
encryptedKey: ciphertext,
|
>
|
||||||
iv,
|
<ModalContent
|
||||||
tag,
|
title={
|
||||||
environment,
|
t("section.token.add-dialog.title", {
|
||||||
secretPath,
|
target: currentWorkspace?.name
|
||||||
expiresIn: Number(expiresIn),
|
}) as string
|
||||||
name,
|
|
||||||
workspaceId: currentWorkspace._id,
|
|
||||||
randomBytes,
|
|
||||||
permissions: Object.entries(permissions)
|
|
||||||
.filter(([, permissionsValue]) => permissionsValue)
|
|
||||||
.map(([permissionsKey]) => permissionsKey)
|
|
||||||
});
|
|
||||||
|
|
||||||
setToken(serviceToken);
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully created a service token",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to create a service token",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
subTitle={t("section.token.add-dialog.description") as string}
|
||||||
|
>
|
||||||
return (
|
{!hasServiceToken ? (
|
||||||
<Modal
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
isOpen={popUp?.createAPIToken?.isOpen}
|
<Controller
|
||||||
onOpenChange={(open) => {
|
control={control}
|
||||||
handlePopUpToggle("createAPIToken", open);
|
name="name"
|
||||||
reset();
|
defaultValue=""
|
||||||
setToken("");
|
render={({ field, fieldState: { error } }) => (
|
||||||
}}
|
<FormControl
|
||||||
>
|
label={t("section.token.add-dialog.name")}
|
||||||
<ModalContent
|
isError={Boolean(error)}
|
||||||
title={
|
errorText={error?.message}
|
||||||
t("section.token.add-dialog.title", {
|
>
|
||||||
target: currentWorkspace?.name
|
<Input {...field} placeholder="Type your token name" />
|
||||||
}) as string
|
</FormControl>
|
||||||
}
|
|
||||||
subTitle={t("section.token.add-dialog.description") as string}
|
|
||||||
>
|
|
||||||
{!hasServiceToken ? (
|
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="name"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label={t("section.token.add-dialog.name")}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="Type your token name" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="environment"
|
|
||||||
defaultValue={currentWorkspace?.environments?.[0]?.slug}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Environment"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{currentWorkspace?.environments.map(({ name, slug }) => (
|
|
||||||
<SelectItem value={slug} key={slug}>
|
|
||||||
{name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="secretPath"
|
|
||||||
defaultValue="/"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Secrets Path"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
helperText="Tokens can be scoped to a folder path. Default path is /"
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="Provide a path, default is /" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="expiresIn"
|
|
||||||
defaultValue={String(apiTokenExpiry?.[0]?.value)}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Expiration"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{apiTokenExpiry.map(({ label, value }) => (
|
|
||||||
<SelectItem value={String(value || "")} key={label}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="permissions"
|
|
||||||
defaultValue={{
|
|
||||||
read: true,
|
|
||||||
write: false
|
|
||||||
}}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
label: "Read (default)",
|
|
||||||
value: "read"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Write (optional)",
|
|
||||||
value: "write"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
label="Permissions"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
{options.map(({ label, value: optionValue }) => {
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
id={value[optionValue]}
|
|
||||||
key={optionValue}
|
|
||||||
className="data-[state=checked]:bg-primary"
|
|
||||||
isChecked={value[optionValue]}
|
|
||||||
isDisabled={optionValue === "read"}
|
|
||||||
onCheckedChange={(state) => {
|
|
||||||
onChange({
|
|
||||||
...value,
|
|
||||||
[optionValue]: state
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Checkbox>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="mt-8 flex items-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4"
|
|
||||||
type="submit"
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
<ModalClose asChild>
|
|
||||||
<Button variant="plain" colorSchema="secondary">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</ModalClose>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
<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">{newToken}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={copyTokenToClipboard}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isTokenCopied ? 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">
|
|
||||||
{t("common.click-to-copy")}
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</ModalContent>
|
/>
|
||||||
</Modal>
|
{tokenScopes.map(({ id }, index) => (
|
||||||
);
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
}
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`scopes.${index}.environment`}
|
||||||
|
defaultValue={currentWorkspace?.environments?.[0]?.slug}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0"
|
||||||
|
label={index === 0 ? "Environment" : undefined}
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map(({ name, slug }) => (
|
||||||
|
<SelectItem value={slug} key={slug}>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`scopes.${index}.secretPath`}
|
||||||
|
defaultValue="/"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
label={index === 0 ? "Secrets Path" : undefined}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="can be /, /nested/**, /**/deep" />
|
||||||
|
</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
|
||||||
|
control={control}
|
||||||
|
name="expiresIn"
|
||||||
|
defaultValue={String(apiTokenExpiry?.[0]?.value)}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Expiration" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{apiTokenExpiry.map(({ label, value }) => (
|
||||||
|
<SelectItem value={String(value || "")} key={label}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="permissions"
|
||||||
|
defaultValue={{
|
||||||
|
read: true,
|
||||||
|
write: false
|
||||||
|
}}
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: "Read (default)",
|
||||||
|
value: "read"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Write (optional)",
|
||||||
|
value: "write"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
label="Permissions"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{options.map(({ label, value: optionValue }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
id={value[optionValue]}
|
||||||
|
key={optionValue}
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
|
isChecked={value[optionValue]}
|
||||||
|
isDisabled={optionValue === "read"}
|
||||||
|
onCheckedChange={(state) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[optionValue]: state
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
type="submit"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button variant="plain" colorSchema="secondary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<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">{newToken}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={copyTokenToClipboard}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isTokenCopied ? 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">
|
||||||
|
{t("common.click-to-copy")}
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+25
-31
@@ -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,32 +43,29 @@ 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">
|
||||||
<Button
|
{t("section.token.service-tokens")}
|
||||||
colorSchema="secondary"
|
</p>
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
<Button
|
||||||
onClick={() => {
|
colorSchema="secondary"
|
||||||
handlePopUpOpen("createAPIToken");
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
handlePopUpOpen("createAPIToken");
|
||||||
Create token
|
}}
|
||||||
</Button>
|
>
|
||||||
|
Create token
|
||||||
|
</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")}
|
||||||
|
|||||||
+76
-65
@@ -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 {
|
||||||
@@ -18,71 +18,82 @@ import { useGetUserWsServiceTokens } from "@app/hooks/api";
|
|||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["deleteAPITokenConfirmation"]>,
|
popUpName: keyof UsePopUpState<["deleteAPITokenConfirmation"]>,
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
id
|
id
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ServiceTokenTable = ({
|
export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
|
||||||
handlePopUpOpen
|
const { currentWorkspace } = useWorkspace();
|
||||||
}: Props) => {
|
const { data, isLoading } = useGetUserWsServiceTokens({
|
||||||
const { currentWorkspace } = useWorkspace();
|
workspaceID: currentWorkspace?._id || ""
|
||||||
const { data, isLoading } = useGetUserWsServiceTokens({
|
});
|
||||||
workspaceID: currentWorkspace?._id || ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
<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 &&
|
||||||
{!isLoading && data && data.map((row) => (
|
data &&
|
||||||
<Tr key={row._id}>
|
data.map((row) => (
|
||||||
<Td>{row.name}</Td>
|
<Tr key={row._id}>
|
||||||
<Td>{row.environment}</Td>
|
<Td>{row.name}</Td>
|
||||||
<Td>{row.secretPath}</Td>
|
<Td>
|
||||||
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
|
<div className="mb-2 flex flex-col flex-wrap space-y-1">
|
||||||
<Td className="flex items-center justify-end">
|
{row?.scopes.map(({ secretPath, environment }) => (
|
||||||
<IconButton
|
<div
|
||||||
onClick={() =>
|
key={`${row._id}-${environment}-${secretPath}`}
|
||||||
handlePopUpOpen("deleteAPITokenConfirmation", {
|
className="inline-flex items-center space-x-1 rounded-md border border-mineshaft-600 p-1 px-2"
|
||||||
name: row.name,
|
>
|
||||||
id: row._id
|
<div className="mr-2 border-r border-mineshaft-600 pr-2">{environment}</div>
|
||||||
})
|
<FontAwesomeIcon icon={faFolder} size="sm" />
|
||||||
}
|
<span className="pl-2">{secretPath}</span>
|
||||||
colorSchema="danger"
|
</div>
|
||||||
ariaLabel="delete"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrashCan} />
|
|
||||||
</IconButton>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
))}
|
||||||
{!isLoading && data && data?.length === 0 && (
|
</div>
|
||||||
<Tr>
|
</Td>
|
||||||
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
|
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
|
||||||
<EmptyState title="No service tokens found" icon={faKey} />
|
<Td>
|
||||||
</Td>
|
<IconButton
|
||||||
</Tr>
|
onClick={() =>
|
||||||
)}
|
handlePopUpOpen("deleteAPITokenConfirmation", {
|
||||||
</TBody>
|
name: row.name,
|
||||||
</Table>
|
id: row._id
|
||||||
</TableContainer>
|
})
|
||||||
);
|
}
|
||||||
}
|
colorSchema="danger"
|
||||||
|
ariaLabel="delete"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrashCan} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
{!isLoading && data && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
|
||||||
|
<EmptyState title="No service tokens found" icon={faKey} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user