From 84d094b4d885c91783db7ddaa762e293487702f0 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 21 Sep 2023 15:15:22 +0100 Subject: [PATCH] Finish preliminary CRUD ops for service token v3, ServiceTokenV3Key structure --- .../v3/serviceTokenDataController.ts | 80 +++- backend/src/helpers/auth.ts | 8 +- backend/src/models/index.ts | 3 +- backend/src/models/serviceToken.ts | 1 + backend/src/models/serviceTokenData.ts | 1 + backend/src/models/serviceTokenDataV3.ts | 38 ++ backend/src/models/serviceTokenDataV3Key.ts | 43 ++ backend/src/validation/serviceTokenV3.ts | 25 +- frontend/src/components/v2/Modal/Modal.tsx | 2 +- .../src/hooks/api/serviceTokens/queries.tsx | 23 +- frontend/src/hooks/api/serviceTokens/types.ts | 18 +- .../AddServiceTokenModal.tsx | 7 +- .../ServiceTokenSection/ServiceTokenTable.tsx | 2 +- .../AddServiceTokenV3Modal.tsx | 418 ++++++++++++++---- .../ServiceTokenV3Section.tsx | 62 ++- .../ServiceTokenV3Table.tsx | 108 +++-- 16 files changed, 668 insertions(+), 171 deletions(-) create mode 100644 backend/src/models/serviceTokenDataV3Key.ts diff --git a/backend/src/controllers/v3/serviceTokenDataController.ts b/backend/src/controllers/v3/serviceTokenDataController.ts index 61ce1f6b3..947cbda68 100644 --- a/backend/src/controllers/v3/serviceTokenDataController.ts +++ b/backend/src/controllers/v3/serviceTokenDataController.ts @@ -1,51 +1,99 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; -import { ServiceTokenDataV3 } from "../../models"; +import { + ServiceTokenDataV3, + ServiceTokenDataV3Key +} from "../../models"; import { validateRequest } from "../../helpers/validation"; import * as reqValidator from "../../validation/serviceTokenV3"; import { createToken } from "../../helpers/auth"; +/** + * Create service token data + * @param req + * @param res + * @returns + */ export const createServiceTokenData = async (req: Request, res: Response) => { const { - body: { name, workspaceId, publicKey } + body: { + name, + workspaceId, + publicKey, + scopes, + expiresIn, + encryptedKey, // for ServiceTokenDataV3Key + nonce // for ServiceTokenDataV3Key + } } = await validateRequest(reqValidator.CreateServiceTokenV3, req); + let expiresAt; + if (expiresIn) { + expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + } + const serviceTokenData = await new ServiceTokenDataV3({ name, workspace: new Types.ObjectId(workspaceId), publicKey, - isActive: false + scopes, + isActive: false, + expiresAt + }).save(); + + await new ServiceTokenDataV3Key({ + encryptedKey, + nonce, + sender: req.user._id, + serviceTokenData: serviceTokenData._id, + workspace: new Types.ObjectId(workspaceId) }).save(); - - console.log("the newly created serviceTokenDataV3: ", serviceTokenData); const token = createToken({ payload: { _id: serviceTokenData._id.toString() }, - expiresIn: "5d", + expiresIn, secret: "hello" // TODO: replace with real secret }); - console.log("jwt token: ", token); - return res.status(200).send({ serviceTokenData, serviceToken: `proj_token.${token}` }); } +/** + * Update service token data with id [serviceTokenDataId] + * @param req + * @param res + * @returns + */ export const updateServiceTokenData = async (req: Request, res: Response) => { const { params: { serviceTokenDataId }, - body: { name, isActive } + body: { + name, + isActive, + scopes, + expiresIn + } } = await validateRequest(reqValidator.UpdateServiceTokenV3, req); + + let expiresAt; + if (expiresIn) { + expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + } const serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate( serviceTokenDataId, { name, - isActive + isActive, + scopes, + expiresAt }, { new: true @@ -57,12 +105,24 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { }); } +/** + * Delete service token data with id [serviceTokenDataId] + * @param req + * @param res + * @returns + */ export const deleteServiceTokenData = async (req: Request, res: Response) => { const { params: { serviceTokenDataId } } = await validateRequest(reqValidator.DeleteServiceTokenV3, req); const serviceTokenData = await ServiceTokenDataV3.findByIdAndDelete(serviceTokenDataId); + + if (serviceTokenData) { + await ServiceTokenDataV3Key.findOneAndDelete({ + serviceTokenData: serviceTokenData._id + }); + } return res.status(200).send({ serviceTokenData diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 74094cba6..be10dbbfa 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -382,11 +382,15 @@ export const createToken = ({ secret, }: { payload: any; - expiresIn: string | number; + expiresIn?: string | number; secret: string; }) => { return jwt.sign(payload, secret, { - expiresIn, + ...( + (expiresIn !== undefined && expiresIn !== null) + ? { expiresIn } + : {} + ) }); }; diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 40fa34d6e..99fc9c4f1 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -28,4 +28,5 @@ export * from "./apiKeyData"; export * from "./loginSRPDetail"; export * from "./tokenVersion"; export * from "./webhooks"; -export * from "./serviceTokenDataV3"; \ No newline at end of file +export * from "./serviceTokenDataV3"; +export * from "./serviceTokenDataV3Key"; diff --git a/backend/src/models/serviceToken.ts b/backend/src/models/serviceToken.ts index 4734a50e2..0e943b177 100644 --- a/backend/src/models/serviceToken.ts +++ b/backend/src/models/serviceToken.ts @@ -1,3 +1,4 @@ +// TODO: deprecate import { Schema, Types, model } from "mongoose"; export interface IServiceToken { _id: Types.ObjectId; diff --git a/backend/src/models/serviceTokenData.ts b/backend/src/models/serviceTokenData.ts index ea7d00eaa..735131703 100644 --- a/backend/src/models/serviceTokenData.ts +++ b/backend/src/models/serviceTokenData.ts @@ -1,3 +1,4 @@ +// TODO: deprecate import { Document, Schema, Types, model } from "mongoose"; export interface IServiceTokenData extends Document { diff --git a/backend/src/models/serviceTokenDataV3.ts b/backend/src/models/serviceTokenDataV3.ts index ee3b733a9..049c2a33c 100644 --- a/backend/src/models/serviceTokenDataV3.ts +++ b/backend/src/models/serviceTokenDataV3.ts @@ -1,5 +1,16 @@ import { Document, Schema, Types, model } from "mongoose"; +enum Permission { + READ = "read", + READ_WRITE = "readWrite" +} + +interface Scope { + environment: string; + secretPath: string; + permission: Permission; +} + export interface IServiceTokenDataV3 extends Document { _id: Types.ObjectId; name: string; @@ -7,6 +18,7 @@ export interface IServiceTokenDataV3 extends Document { publicKey: string; isActive: boolean; lastUsed: Date; + scopes: Array; } const serviceTokenDataV3Schema = new Schema( @@ -31,6 +43,32 @@ const serviceTokenDataV3Schema = new Schema( lastUsed: { type: Date, required: false + }, + expiresAt: { + type: Date, + required: false, + expires: 0 + }, + scopes: { + type: [ + { + environment: { + type: String, + required: true + }, + secretPath: { + type: String, + default: "/", + required: true + }, + permission: { + type: String, + enum: [Permission.READ, Permission.READ_WRITE], + required: true + } + } + ], + required: true } }, { diff --git a/backend/src/models/serviceTokenDataV3Key.ts b/backend/src/models/serviceTokenDataV3Key.ts new file mode 100644 index 000000000..7a69abc1e --- /dev/null +++ b/backend/src/models/serviceTokenDataV3Key.ts @@ -0,0 +1,43 @@ +import { Document, Schema, Types, model } from "mongoose"; + +export interface IServiceTokenDataV3Key extends Document { + _id: Types.ObjectId; + encryptedKey: string; + nonce: string; + sender: Types.ObjectId; + serviceTokenData: Types.ObjectId; + workspace: Types.ObjectId; +} + +const serviceTokenDataV3KeySchema = new Schema( + { + encryptedKey: { + type: String, + required: true + }, + nonce: { + type: String, + required: true + }, + sender: { + type: Schema.Types.ObjectId, + ref: "User", + required: true + }, + serviceTokenData: { + type: Schema.Types.ObjectId, + ref: "ServiceTokenDataV3", + required: true, + }, + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, + } + }, + { + timestamps: true + } +); + +export const ServiceTokenDataV3Key = model("ServiceTokenDataV3Key", serviceTokenDataV3KeySchema); \ No newline at end of file diff --git a/backend/src/validation/serviceTokenV3.ts b/backend/src/validation/serviceTokenV3.ts index 2f8cba9f0..377bba287 100644 --- a/backend/src/validation/serviceTokenV3.ts +++ b/backend/src/validation/serviceTokenV3.ts @@ -5,6 +5,17 @@ export const CreateServiceTokenV3 = z.object({ name: z.string().trim(), workspaceId: z.string().trim(), publicKey: z.string().trim(), + scopes: z + .object({ + permission: z.enum(["read", "readWrite"]), + environment: z.string().trim(), + secretPath: z.string().trim() + }) + .array() + .min(1), + expiresIn: z.number().optional(), + encryptedKey: z.string().trim(), + nonce: z.string().trim() }) }); @@ -14,8 +25,18 @@ export const UpdateServiceTokenV3 = z.object({ }), body: z.object({ name: z.string().trim().optional(), - isActive: z.boolean().optional() - }) + isActive: z.boolean().optional(), + scopes: z + .object({ + permission: z.enum(["read", "readWrite"]), + environment: z.string().trim(), + secretPath: z.string().trim() + }) + .array() + .min(1) + .optional(), + expiresIn: z.number().optional() + }), }); export const DeleteServiceTokenV3 = z.object({ diff --git a/frontend/src/components/v2/Modal/Modal.tsx b/frontend/src/components/v2/Modal/Modal.tsx index 200ceefb2..4c5b4be48 100644 --- a/frontend/src/components/v2/Modal/Modal.tsx +++ b/frontend/src/components/v2/Modal/Modal.tsx @@ -29,7 +29,7 @@ export const ModalContent = forwardRef( diff --git a/frontend/src/hooks/api/serviceTokens/queries.tsx b/frontend/src/hooks/api/serviceTokens/queries.tsx index eda5696be..d05522f7b 100644 --- a/frontend/src/hooks/api/serviceTokens/queries.tsx +++ b/frontend/src/hooks/api/serviceTokens/queries.tsx @@ -2,18 +2,17 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { workspaceKeys } from "../workspace/queries"; import { + CreateServiceTokenDataV3DTO, + CreateServiceTokenDataV3Res, CreateServiceTokenDTO, CreateServiceTokenRes, + DeleteServiceTokenDataV3DTO, DeleteServiceTokenRes, ServiceToken, ServiceTokenDataV3, - CreateServiceTokenDataV3DTO, - CreateServiceTokenDataV3Res, - UpdateServiceTokenDataV3DTO, - DeleteServiceTokenDataV3DTO -} from "./types"; -import { workspaceKeys } from "../workspace/queries"; + UpdateServiceTokenDataV3DTO} from "./types"; const serviceTokenKeys = { getAllWorkspaceServiceToken: (workspaceID: string) => [{ workspaceID }, "service-tokens"] as const @@ -58,7 +57,6 @@ export const useDeleteServiceToken = () => { return useMutation({ mutationFn: async (serviceTokenId) => { - console.log("useDeleteServiceToken"); const { data } = await apiRequest.delete(`/api/v2/service-token/${serviceTokenId}`); return data; }, @@ -87,11 +85,15 @@ export const useUpdateServiceTokenV3 = () => { mutationFn: async ({ serviceTokenDataId, name, - isActive + isActive, + scopes, + expiresIn }) => { const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, { name, - isActive + isActive, + scopes, + expiresIn }); return serviceTokenData; @@ -108,13 +110,10 @@ export const useDeleteServiceTokenV3 = () => { mutationFn: async ({ serviceTokenDataId }) => { - console.log("useDeleteServiceTokenV3"); const { data: { serviceTokenData } } = await apiRequest.delete(`/api/v3/service-token/${serviceTokenDataId}`); - console.log("useDeleteServiceTokenV3 serviceTokenData: ", serviceTokenData); return serviceTokenData; }, onSuccess: ({ workspace }) => { - console.log("useDeleteServiceTokenV3 onSuccess: ", workspace); queryClient.invalidateQueries(workspaceKeys.getWorkspaceServiceTokenDataV3(workspace)); } }); diff --git a/frontend/src/hooks/api/serviceTokens/types.ts b/frontend/src/hooks/api/serviceTokens/types.ts index bb88d18fe..d26fca99c 100644 --- a/frontend/src/hooks/api/serviceTokens/types.ts +++ b/frontend/src/hooks/api/serviceTokens/types.ts @@ -36,22 +36,32 @@ export type DeleteServiceTokenRes = { serviceTokenData: ServiceToken }; // --- v3 +export type ServiceTokenV3Scope = { + permission: string; + environment: string; + secretPath: string; +}; + export type ServiceTokenDataV3 = { _id: string; name: string; workspace: string; isActive: boolean; lastUsed?: string; + scopes: ServiceTokenV3Scope[]; + expiresAt?: string; createdAt: string; updatedAt: string; }; -// TODO: add scopes -// TODO: encrypted key info export type CreateServiceTokenDataV3DTO = { name: string; workspaceId: string; publicKey: string; + scopes: ServiceTokenV3Scope[]; + expiresIn?: number; + encryptedKey: string; + nonce: string; } export type CreateServiceTokenDataV3Res = { @@ -61,8 +71,10 @@ export type CreateServiceTokenDataV3Res = { export type UpdateServiceTokenDataV3DTO = { serviceTokenDataId: string; - name?: string; isActive?: boolean; + name?: string; + scopes?: ServiceTokenV3Scope[]; + expiresIn?: number; } export type DeleteServiceTokenDataV3DTO = { diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/AddServiceTokenModal.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/AddServiceTokenModal.tsx index a44db233b..7910652b5 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/AddServiceTokenModal.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/AddServiceTokenModal.tsx @@ -88,7 +88,10 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { } = useForm({ resolver: yupResolver(schema), defaultValues: { - scopes: [{ secretPath: "/", environment: currentWorkspace?.environments?.[0]?.slug }] + scopes: [{ + secretPath: "/", + environment: currentWorkspace?.environments?.[0]?.slug + }] } }); @@ -133,7 +136,7 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { plaintext: key, key: randomBytes }); - + const { serviceToken } = await createServiceToken.mutateAsync({ encryptedKey: ciphertext, iv, diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx index 9c71e4408..6ac54ba97 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenTable.tsx @@ -43,7 +43,7 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => { Token Name - Envrionment - Secret Path + Environment - Secret Path Valid Until diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx index f3eebb2d6..1a88d1207 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx @@ -1,23 +1,41 @@ +import { useEffect } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus,faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { yupResolver } from "@hookform/resolvers/yup"; import nacl from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; -import { UsePopUpState } from "@app/hooks/usePopUp"; -import { Controller, useForm } from "react-hook-form"; -import { useWorkspace } from "@app/context"; +import * as yup from "yup"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { + decryptAssymmetric, + encryptAssymmetric +} from "@app/components/utilities/cryptography/crypto"; +import { + Button, + FormControl, + IconButton, + Input, Modal, ModalContent, - FormControl, Select, SelectItem, - Input, - Button + // Accordion, + // AccordionItem, + // AccordionTrigger, + // AccordionContent } from "@app/components/v2"; -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; -import { useCreateServiceTokenV3 } from "@app/hooks/api"; +import { useWorkspace } from "@app/context"; +import { + useCreateServiceTokenV3, + useGetUserWsKey, + useUpdateServiceTokenV3 +} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; const expirations = [ + { label: "Never", value: undefined }, { label: "1 day", value: "86400" }, { label: "7 days", value: "604800" }, { label: "1 month", value: "2592000" }, @@ -25,88 +43,172 @@ const expirations = [ { label: "12 months", value: "31104000" } ]; +const permissionValues: Array<"read" | "readWrite"> = ["read", "readWrite"]; + const schema = yup.object({ name: yup.string().required("ST V3 name is required"), - expiresIn: yup.string().required("ST V3 expiration window is required") + expiresIn: yup.string(), + scopes: yup + .array( + yup.object({ + permission: yup.string().oneOf(permissionValues, "Invalid permission").required().label("Permission"), + environment: yup.string().max(50).required().label("Environment"), + secretPath: yup + .string() + .required() + .default("/") + .label("Secret Path") + .transform((val) => + typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val + ) + }) + ) + .min(1) + .required() + .label("Scope") }).required(); export type FormData = yup.InferType; type Props = { - popUp: UsePopUpState<["createServiceTokenV3"]>; - handlePopUpToggle: (popUpName: keyof UsePopUpState<["createServiceTokenV3"]>, state?: boolean) => void; + popUp: UsePopUpState<["serviceTokenV3"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["serviceTokenV3"]>, state?: boolean) => void; }; -// Will download a JSON -// Maybe you can set a timer at which point service token is no longer active! -// Maybe you can also set IP allowlist for it too - export const AddServiceTokenV3Modal = ({ popUp, handlePopUpToggle }: Props) => { const { currentWorkspace } = useWorkspace(); - const { mutateAsync } = useCreateServiceTokenV3(); + + const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? ""); + const { mutateAsync: createMutateAsync } = useCreateServiceTokenV3(); + const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3(); const { createNotification } = useNotificationContext(); const { control, handleSubmit, - reset + reset, + formState: { isSubmitting } } = useForm({ - resolver: yupResolver(schema) + resolver: yupResolver(schema), + defaultValues: { + name: "", + scopes: [{ + permission: "read", + environment: currentWorkspace?.environments?.[0]?.slug, + secretPath: "/", + }] + } }); - + + useEffect(() => { + const serviceTokenData = popUp?.serviceTokenV3?.data as { + serviceTokenDataId: string; + name: string; + scopes: any; + }; + + if (serviceTokenData) { + reset({ + name: serviceTokenData.name, + scopes: serviceTokenData.scopes + }); + } else { + reset({ + name: "", + scopes: [{ + permission: "read", + environment: currentWorkspace?.environments?.[0]?.slug, + secretPath: "/", + }] + }); + } + }, [popUp?.serviceTokenV3?.data]); + + const { fields: tokenScopes, append, remove } = useFieldArray({ control, name: "scopes" }); + const onFormSubmit = async ({ name, - expiresIn + expiresIn, + scopes }: FormData) => { try { - if (!currentWorkspace?._id) return; - - console.log("onFormSubmit name: ", name); - console.log("onFormSubmit expiresIn: ", expiresIn); - - const pair = nacl.box.keyPair(); - const secretKeyUint8Array = pair.secretKey; - const publicKeyUint8Array = pair.publicKey; - const privateKey = encodeBase64(secretKeyUint8Array); - const publicKey = encodeBase64(publicKeyUint8Array); - - console.log("pair: ", pair); - console.log("privateKey: ", privateKey); - console.log("publicKey: ", publicKey ); - const { serviceToken } = await mutateAsync({ - name, - workspaceId: currentWorkspace._id, - publicKey - }); - - const downloadData = { - publicKey, - privateKey, - serviceToken + const serviceTokenData = popUp?.serviceTokenV3?.data as { + serviceTokenDataId: string; + name: string; + scopes: any; }; + + if (serviceTokenData) { + // update + await updateMutateAsync({ + serviceTokenDataId: serviceTokenData.serviceTokenDataId, + name, + scopes, + expiresIn: expiresIn === "" ? undefined : Number(expiresIn) + }); + } else { + // create + if (!currentWorkspace?._id) return; + if (!latestFileKey) return; + + const pair = nacl.box.keyPair(); + const secretKeyUint8Array = pair.secretKey; + const publicKeyUint8Array = pair.publicKey; + const privateKey = encodeBase64(secretKeyUint8Array); + const publicKey = encodeBase64(publicKeyUint8Array); + + const key = decryptAssymmetric({ + ciphertext: latestFileKey.encryptedKey, + nonce: latestFileKey.nonce, + publicKey: latestFileKey.sender.publicKey, + privateKey: localStorage.getItem("PRIVATE_KEY") as string + }); + + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: key, + publicKey, + privateKey: localStorage.getItem("PRIVATE_KEY") as string + }); - const blob = new Blob([JSON.stringify(downloadData, null, 2)], { type: 'application/json' }); - const href = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = href; - link.download = `infisical_${name}.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + const { serviceToken } = await createMutateAsync({ + name, + workspaceId: currentWorkspace._id, + publicKey, + scopes, + expiresIn: expiresIn === "" ? undefined : Number(expiresIn), + encryptedKey: ciphertext, + nonce + }); + + const downloadData = { + publicKey, + privateKey, + serviceToken + }; + + const blob = new Blob([JSON.stringify(downloadData, null, 2)], { type: "application/json" }); + const href = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = href; + link.download = `infisical_${name}.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } createNotification({ - text: "Successfully created ST V3", + text: `Successfully ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`, type: "success" }); reset(); - handlePopUpToggle("createServiceTokenV3", false); + handlePopUpToggle("serviceTokenV3", false); } catch (err) { console.error(err); createNotification({ - text: "Failed to create ST V3", + text: `Failed to ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`, type: "error" }); } @@ -114,13 +216,13 @@ export const AddServiceTokenV3Modal = ({ return ( { - handlePopUpToggle("createServiceTokenV3", isOpen); + handlePopUpToggle("serviceTokenV3", isOpen); reset(); }} > - +
)} /> - ( - ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + remove(index)} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx index 9ca14ef9c..ac2ece7c0 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx @@ -1,14 +1,47 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { AddServiceTokenV3Modal } from "./AddServiceTokenV3Modal"; -import { ServiceTokenV3Table } from "./ServiceTokenV3Table"; -import { Button } from "@app/components/v2"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + Button, + DeleteActionModal +} from "@app/components/v2"; +import { + useDeleteServiceTokenV3 +} from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; +import { AddServiceTokenV3Modal } from "./AddServiceTokenV3Modal"; +import { ServiceTokenV3Table } from "./ServiceTokenV3Table"; + export const ServiceTokenV3Section = () => { - const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ - "createServiceTokenV3" + const { createNotification } = useNotificationContext(); + const { mutateAsync: deleteMutateAsync } = useDeleteServiceTokenV3(); + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "serviceTokenV3", + "deleteServiceTokenV3" ] 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 (
@@ -19,16 +52,31 @@ export const ServiceTokenV3Section = () => { colorSchema="secondary" type="submit" leftIcon={} - onClick={() => handlePopUpOpen("createServiceTokenV3")} + onClick={() => handlePopUpOpen("serviceTokenV3")} > Create ST V3
- + + handlePopUpToggle("deleteServiceTokenV3", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onDeleteServiceTokenDataSubmit( + (popUp?.deleteServiceTokenV3?.data as { serviceTokenDataId: string })?.serviceTokenDataId + ) + } + />
); } \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx index 66a4c7025..fcade0890 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx @@ -1,6 +1,6 @@ +import { faKey, faPencil,faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faKey, faXmark, faPencil } from "@fortawesome/free-solid-svg-icons"; -import { useWorkspace } from "@app/context"; + import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { EmptyState, @@ -15,38 +15,34 @@ import { THead, Tr } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; import { useGetWorkspaceServiceTokenDataV3, - useUpdateServiceTokenV3, - useDeleteServiceTokenV3 + useUpdateServiceTokenV3 } from "@app/hooks/api"; +import { + ServiceTokenV3Scope +} from "@app/hooks/api/serviceTokens/types" +import { UsePopUpState } from "@app/hooks/usePopUp"; -export const ServiceTokenV3Table = () => { +type Props = { + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["deleteServiceTokenV3", "serviceTokenV3"]>, + data?: { + serviceTokenDataId?: string; + name?: string; + scopes?: ServiceTokenV3Scope[]; + } + ) => void; + }; + +export const ServiceTokenV3Table = ({ + handlePopUpOpen +}: Props) => { const { createNotification } = useNotificationContext(); const { currentWorkspace } = useWorkspace(); const { data, isLoading } = useGetWorkspaceServiceTokenDataV3(currentWorkspace?._id || ""); const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3(); - const { mutateAsync: deleteMutateAsync } = useDeleteServiceTokenV3(); - - console.log("data1: ", data); - - const handleDeleteServiceTokenData = async (serviceTokenDataId: string) => { - try { - await deleteMutateAsync({ - serviceTokenDataId - }); - createNotification({ - text: "Successfully deleted service token v3", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete service token v3", - type: "error" - }); - } - } const handleToggleServiceTokenDataStatus = async ({ serviceTokenDataId, @@ -92,10 +88,11 @@ export const ServiceTokenV3Table = () => { Name Status - Last Active - Created - Expiration - + Scopes + Last Used + Created At + Expires At + @@ -108,32 +105,48 @@ export const ServiceTokenV3Table = () => { name, isActive, lastUsed, + scopes, createdAt, - // expiresAt + expiresAt }) => { return ( {name} - handleToggleServiceTokenDataStatus({ - serviceTokenDataId: _id, - isActive: value - })} - isChecked={isActive} - > -

{isActive ? "Active" : "Inactive"}

-
+ handleToggleServiceTokenDataStatus({ + serviceTokenDataId: _id, + isActive: value + })} + isChecked={isActive} + > +

{isActive ? "Active" : "Inactive"}

+
+ + {scopes.map((scope) => { + return ( +

+ + {scope.permission} + + {` @${scope.environment} - ${scope.secretPath}`} +

+ ); + })} + {lastUsed ? formatDate(lastUsed) : "-"} {formatDate(createdAt)} - {formatDate(createdAt)} + {expiresAt ? formatDate(expiresAt) : "-"} { - console.log("edit"); + handlePopUpOpen("serviceTokenV3", { + serviceTokenDataId: _id, + name, + scopes, + }); }} size="lg" colorSchema="primary" @@ -143,7 +156,12 @@ export const ServiceTokenV3Table = () => { handleDeleteServiceTokenData(_id)} + onClick={() => { + handlePopUpOpen("deleteServiceTokenV3", { + serviceTokenDataId: _id, + name + }); + }} size="lg" colorSchema="danger" variant="plain"