Finish preliminary CRUD ops for service token v3, ServiceTokenV3Key structure

This commit is contained in:
Tuan Dang
2023-09-21 15:15:22 +01:00
parent efb14ca267
commit 84d094b4d8
16 changed files with 668 additions and 171 deletions
@@ -1,51 +1,99 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { ServiceTokenDataV3 } from "../../models"; import {
ServiceTokenDataV3,
ServiceTokenDataV3Key
} from "../../models";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/serviceTokenV3"; import * as reqValidator from "../../validation/serviceTokenV3";
import { createToken } from "../../helpers/auth"; import { createToken } from "../../helpers/auth";
/**
* Create service token data
* @param req
* @param res
* @returns
*/
export const createServiceTokenData = async (req: Request, res: Response) => { export const createServiceTokenData = async (req: Request, res: Response) => {
const { const {
body: { name, workspaceId, publicKey } body: {
name,
workspaceId,
publicKey,
scopes,
expiresIn,
encryptedKey, // for ServiceTokenDataV3Key
nonce // for ServiceTokenDataV3Key
}
} = await validateRequest(reqValidator.CreateServiceTokenV3, req); } = await validateRequest(reqValidator.CreateServiceTokenV3, req);
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
const serviceTokenData = await new ServiceTokenDataV3({ const serviceTokenData = await new ServiceTokenDataV3({
name, name,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
publicKey, publicKey,
isActive: false scopes,
isActive: false,
expiresAt
}).save(); }).save();
console.log("the newly created serviceTokenDataV3: ", serviceTokenData); await new ServiceTokenDataV3Key({
encryptedKey,
nonce,
sender: req.user._id,
serviceTokenData: serviceTokenData._id,
workspace: new Types.ObjectId(workspaceId)
}).save();
const token = createToken({ const token = createToken({
payload: { payload: {
_id: serviceTokenData._id.toString() _id: serviceTokenData._id.toString()
}, },
expiresIn: "5d", expiresIn,
secret: "hello" // TODO: replace with real secret secret: "hello" // TODO: replace with real secret
}); });
console.log("jwt token: ", token);
return res.status(200).send({ return res.status(200).send({
serviceTokenData, serviceTokenData,
serviceToken: `proj_token.${token}` serviceToken: `proj_token.${token}`
}); });
} }
/**
* Update service token data with id [serviceTokenDataId]
* @param req
* @param res
* @returns
*/
export const updateServiceTokenData = async (req: Request, res: Response) => { export const updateServiceTokenData = async (req: Request, res: Response) => {
const { const {
params: { serviceTokenDataId }, params: { serviceTokenDataId },
body: { name, isActive } body: {
name,
isActive,
scopes,
expiresIn
}
} = await validateRequest(reqValidator.UpdateServiceTokenV3, req); } = await validateRequest(reqValidator.UpdateServiceTokenV3, req);
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
const serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate( const serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenDataId, serviceTokenDataId,
{ {
name, name,
isActive isActive,
scopes,
expiresAt
}, },
{ {
new: true new: true
@@ -57,6 +105,12 @@ 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) => { export const deleteServiceTokenData = async (req: Request, res: Response) => {
const { const {
params: { serviceTokenDataId } params: { serviceTokenDataId }
@@ -64,6 +118,12 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
const serviceTokenData = await ServiceTokenDataV3.findByIdAndDelete(serviceTokenDataId); const serviceTokenData = await ServiceTokenDataV3.findByIdAndDelete(serviceTokenDataId);
if (serviceTokenData) {
await ServiceTokenDataV3Key.findOneAndDelete({
serviceTokenData: serviceTokenData._id
});
}
return res.status(200).send({ return res.status(200).send({
serviceTokenData serviceTokenData
}); });
+6 -2
View File
@@ -382,11 +382,15 @@ export const createToken = ({
secret, secret,
}: { }: {
payload: any; payload: any;
expiresIn: string | number; expiresIn?: string | number;
secret: string; secret: string;
}) => { }) => {
return jwt.sign(payload, secret, { return jwt.sign(payload, secret, {
expiresIn, ...(
(expiresIn !== undefined && expiresIn !== null)
? { expiresIn }
: {}
)
}); });
}; };
+1
View File
@@ -29,3 +29,4 @@ export * from "./loginSRPDetail";
export * from "./tokenVersion"; export * from "./tokenVersion";
export * from "./webhooks"; export * from "./webhooks";
export * from "./serviceTokenDataV3"; export * from "./serviceTokenDataV3";
export * from "./serviceTokenDataV3Key";
+1
View File
@@ -1,3 +1,4 @@
// TODO: deprecate
import { Schema, Types, model } from "mongoose"; import { Schema, Types, model } from "mongoose";
export interface IServiceToken { export interface IServiceToken {
_id: Types.ObjectId; _id: Types.ObjectId;
+1
View File
@@ -1,3 +1,4 @@
// TODO: deprecate
import { Document, Schema, Types, model } from "mongoose"; import { Document, Schema, Types, model } from "mongoose";
export interface IServiceTokenData extends Document { export interface IServiceTokenData extends Document {
+38
View File
@@ -1,5 +1,16 @@
import { Document, Schema, Types, model } from "mongoose"; 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 { export interface IServiceTokenDataV3 extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
name: string; name: string;
@@ -7,6 +18,7 @@ export interface IServiceTokenDataV3 extends Document {
publicKey: string; publicKey: string;
isActive: boolean; isActive: boolean;
lastUsed: Date; lastUsed: Date;
scopes: Array<Scope>;
} }
const serviceTokenDataV3Schema = new Schema( const serviceTokenDataV3Schema = new Schema(
@@ -31,6 +43,32 @@ const serviceTokenDataV3Schema = new Schema(
lastUsed: { lastUsed: {
type: Date, type: Date,
required: false 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
} }
}, },
{ {
@@ -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<IServiceTokenDataV3Key>("ServiceTokenDataV3Key", serviceTokenDataV3KeySchema);
+23 -2
View File
@@ -5,6 +5,17 @@ export const CreateServiceTokenV3 = z.object({
name: z.string().trim(), name: z.string().trim(),
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
publicKey: 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({ body: z.object({
name: z.string().trim().optional(), 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({ export const DeleteServiceTokenV3 = z.object({
+1 -1
View File
@@ -29,7 +29,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
<Card <Card
isRounded isRounded
className={twMerge( className={twMerge(
"fixed top-1/2 left-1/2 z-[90] dark:[color-scheme:dark] max-h-screen overflow-y-auto thin-scrollbar max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl", "fixed top-1/2 left-1/2 z-[90] dark:[color-scheme:dark] max-h-screen overflow-y-auto thin-scrollbar max-w-xl -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl",
className className
)} )}
> >
@@ -2,18 +2,17 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries";
import { import {
CreateServiceTokenDataV3DTO,
CreateServiceTokenDataV3Res,
CreateServiceTokenDTO, CreateServiceTokenDTO,
CreateServiceTokenRes, CreateServiceTokenRes,
DeleteServiceTokenDataV3DTO,
DeleteServiceTokenRes, DeleteServiceTokenRes,
ServiceToken, ServiceToken,
ServiceTokenDataV3, ServiceTokenDataV3,
CreateServiceTokenDataV3DTO, UpdateServiceTokenDataV3DTO} from "./types";
CreateServiceTokenDataV3Res,
UpdateServiceTokenDataV3DTO,
DeleteServiceTokenDataV3DTO
} from "./types";
import { workspaceKeys } from "../workspace/queries";
const serviceTokenKeys = { const serviceTokenKeys = {
getAllWorkspaceServiceToken: (workspaceID: string) => [{ workspaceID }, "service-tokens"] as const getAllWorkspaceServiceToken: (workspaceID: string) => [{ workspaceID }, "service-tokens"] as const
@@ -58,7 +57,6 @@ export const useDeleteServiceToken = () => {
return useMutation<DeleteServiceTokenRes, {}, string>({ return useMutation<DeleteServiceTokenRes, {}, string>({
mutationFn: async (serviceTokenId) => { mutationFn: async (serviceTokenId) => {
console.log("useDeleteServiceToken");
const { data } = await apiRequest.delete(`/api/v2/service-token/${serviceTokenId}`); const { data } = await apiRequest.delete(`/api/v2/service-token/${serviceTokenId}`);
return data; return data;
}, },
@@ -87,11 +85,15 @@ export const useUpdateServiceTokenV3 = () => {
mutationFn: async ({ mutationFn: async ({
serviceTokenDataId, serviceTokenDataId,
name, name,
isActive isActive,
scopes,
expiresIn
}) => { }) => {
const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, { const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, {
name, name,
isActive isActive,
scopes,
expiresIn
}); });
return serviceTokenData; return serviceTokenData;
@@ -108,13 +110,10 @@ export const useDeleteServiceTokenV3 = () => {
mutationFn: async ({ mutationFn: async ({
serviceTokenDataId serviceTokenDataId
}) => { }) => {
console.log("useDeleteServiceTokenV3");
const { data: { serviceTokenData } } = await apiRequest.delete(`/api/v3/service-token/${serviceTokenDataId}`); const { data: { serviceTokenData } } = await apiRequest.delete(`/api/v3/service-token/${serviceTokenDataId}`);
console.log("useDeleteServiceTokenV3 serviceTokenData: ", serviceTokenData);
return serviceTokenData; return serviceTokenData;
}, },
onSuccess: ({ workspace }) => { onSuccess: ({ workspace }) => {
console.log("useDeleteServiceTokenV3 onSuccess: ", workspace);
queryClient.invalidateQueries(workspaceKeys.getWorkspaceServiceTokenDataV3(workspace)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceServiceTokenDataV3(workspace));
} }
}); });
+15 -3
View File
@@ -36,22 +36,32 @@ export type DeleteServiceTokenRes = { serviceTokenData: ServiceToken };
// --- v3 // --- v3
export type ServiceTokenV3Scope = {
permission: string;
environment: string;
secretPath: string;
};
export type ServiceTokenDataV3 = { export type ServiceTokenDataV3 = {
_id: string; _id: string;
name: string; name: string;
workspace: string; workspace: string;
isActive: boolean; isActive: boolean;
lastUsed?: string; lastUsed?: string;
scopes: ServiceTokenV3Scope[];
expiresAt?: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
// TODO: add scopes
// TODO: encrypted key info
export type CreateServiceTokenDataV3DTO = { export type CreateServiceTokenDataV3DTO = {
name: string; name: string;
workspaceId: string; workspaceId: string;
publicKey: string; publicKey: string;
scopes: ServiceTokenV3Scope[];
expiresIn?: number;
encryptedKey: string;
nonce: string;
} }
export type CreateServiceTokenDataV3Res = { export type CreateServiceTokenDataV3Res = {
@@ -61,8 +71,10 @@ export type CreateServiceTokenDataV3Res = {
export type UpdateServiceTokenDataV3DTO = { export type UpdateServiceTokenDataV3DTO = {
serviceTokenDataId: string; serviceTokenDataId: string;
name?: string;
isActive?: boolean; isActive?: boolean;
name?: string;
scopes?: ServiceTokenV3Scope[];
expiresIn?: number;
} }
export type DeleteServiceTokenDataV3DTO = { export type DeleteServiceTokenDataV3DTO = {
@@ -88,7 +88,10 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
} = useForm<FormData>({ } = useForm<FormData>({
resolver: yupResolver(schema), resolver: yupResolver(schema),
defaultValues: { defaultValues: {
scopes: [{ secretPath: "/", environment: currentWorkspace?.environments?.[0]?.slug }] scopes: [{
secretPath: "/",
environment: currentWorkspace?.environments?.[0]?.slug
}]
} }
}); });
@@ -43,7 +43,7 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
<THead> <THead>
<Tr> <Tr>
<Th>Token Name</Th> <Th>Token Name</Th>
<Th>Envrionment - Secret Path</Th> <Th>Environment - Secret Path</Th>
<Th>Valid Until</Th> <Th>Valid Until</Th>
<Th aria-label="button" /> <Th aria-label="button" />
</Tr> </Tr>
@@ -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 nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util"; import { encodeBase64 } from "tweetnacl-util";
import { UsePopUpState } from "@app/hooks/usePopUp"; import * as yup from "yup";
import { Controller, useForm } from "react-hook-form";
import { useWorkspace } from "@app/context"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import {
Button,
FormControl,
IconButton,
Input,
Modal, Modal,
ModalContent, ModalContent,
FormControl,
Select, Select,
SelectItem, SelectItem,
Input, // Accordion,
Button // AccordionItem,
// AccordionTrigger,
// AccordionContent
} from "@app/components/v2"; } from "@app/components/v2";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useWorkspace } from "@app/context";
import { yupResolver } from "@hookform/resolvers/yup"; import {
import * as yup from "yup"; useCreateServiceTokenV3,
import { useCreateServiceTokenV3 } from "@app/hooks/api"; useGetUserWsKey,
useUpdateServiceTokenV3
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const expirations = [ const expirations = [
{ label: "Never", value: undefined },
{ label: "1 day", value: "86400" }, { label: "1 day", value: "86400" },
{ label: "7 days", value: "604800" }, { label: "7 days", value: "604800" },
{ label: "1 month", value: "2592000" }, { label: "1 month", value: "2592000" },
@@ -25,88 +43,172 @@ const expirations = [
{ label: "12 months", value: "31104000" } { label: "12 months", value: "31104000" }
]; ];
const permissionValues: Array<"read" | "readWrite"> = ["read", "readWrite"];
const schema = yup.object({ const schema = yup.object({
name: yup.string().required("ST V3 name is required"), 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(); }).required();
export type FormData = yup.InferType<typeof schema>; export type FormData = yup.InferType<typeof schema>;
type Props = { type Props = {
popUp: UsePopUpState<["createServiceTokenV3"]>; popUp: UsePopUpState<["serviceTokenV3"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["createServiceTokenV3"]>, state?: boolean) => void; 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 = ({ export const AddServiceTokenV3Modal = ({
popUp, popUp,
handlePopUpToggle handlePopUpToggle
}: Props) => { }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { mutateAsync } = useCreateServiceTokenV3();
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
const { mutateAsync: createMutateAsync } = useCreateServiceTokenV3();
const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { const {
control, control,
handleSubmit, handleSubmit,
reset reset,
formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
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 ({ const onFormSubmit = async ({
name, name,
expiresIn expiresIn,
scopes
}: FormData) => { }: FormData) => {
try { try {
if (!currentWorkspace?._id) return; const serviceTokenData = popUp?.serviceTokenV3?.data as {
serviceTokenDataId: string;
console.log("onFormSubmit name: ", name); name: string;
console.log("onFormSubmit expiresIn: ", expiresIn); scopes: any;
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 blob = new Blob([JSON.stringify(downloadData, null, 2)], { type: 'application/json' }); if (serviceTokenData) {
const href = URL.createObjectURL(blob); // update
const link = document.createElement('a'); await updateMutateAsync({
link.href = href; serviceTokenDataId: serviceTokenData.serviceTokenDataId,
link.download = `infisical_${name}.json`; name,
document.body.appendChild(link); scopes,
link.click(); expiresIn: expiresIn === "" ? undefined : Number(expiresIn)
document.body.removeChild(link); });
} 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 { 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({ createNotification({
text: "Successfully created ST V3", text: `Successfully ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`,
type: "success" type: "success"
}); });
reset(); reset();
handlePopUpToggle("createServiceTokenV3", false); handlePopUpToggle("serviceTokenV3", false);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
createNotification({ createNotification({
text: "Failed to create ST V3", text: `Failed to ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`,
type: "error" type: "error"
}); });
} }
@@ -114,13 +216,13 @@ export const AddServiceTokenV3Modal = ({
return ( return (
<Modal <Modal
isOpen={popUp?.createServiceTokenV3?.isOpen} isOpen={popUp?.serviceTokenV3?.isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle("createServiceTokenV3", isOpen); handlePopUpToggle("serviceTokenV3", isOpen);
reset(); reset();
}} }}
> >
<ModalContent title="Create Service Token V3"> <ModalContent title={`${popUp?.serviceTokenV3?.data ? "Update" : "Create"} Service Token V3`}>
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
<Controller <Controller
control={control} control={control}
@@ -139,38 +241,184 @@ export const AddServiceTokenV3Modal = ({
</FormControl> </FormControl>
)} )}
/> />
<Controller {tokenScopes.map(({ id }, index) => (
control={control} <div className="flex items-end space-x-2 mb-3" key={id}>
name="expiresIn" <Controller
defaultValue="15552000" control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( name={`scopes.${index}.permission`}
<FormControl defaultValue={currentWorkspace?.environments?.[0]?.slug}
label="Expiration" render={({ field: { onChange, ...field }, fieldState: { error } }) => (
errorText={error?.message} <FormControl
isError={Boolean(error)} className="mb-0"
label={index === 0 ? "Permission" : undefined}
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-36"
>
<SelectItem value="read" key="st-v3-read">
Read
</SelectItem>
<SelectItem value="readWrite" key="st-v3-write">
Read &amp; Write
</SelectItem>
</Select>
</FormControl>
)}
/>
<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-36"
>
{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
onClick={() => remove(index)}
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={() =>
append({
permission: "read",
environment: currentWorkspace?.environments?.[0]?.slug || "",
secretPath: "/"
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
> >
<Select Add Scope
defaultValue={field.value} </Button>
{...field} </div>
onValueChange={(e) => onChange(e)} <Controller
className="w-full" control={control}
name="expiresIn"
defaultValue="15552000"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.serviceTokenV3?.data ? "Update" : ""} Expire In`}
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
> >
{expirations.map(({ label, value }) => ( <Select
<SelectItem value={String(value || "")} key={`api-key-expiration-${label}`}> defaultValue={field.value}
{label} {...field}
</SelectItem> onValueChange={(e) => onChange(e)}
))} className="w-full"
</Select> >
</FormControl> {expirations.map(({ label, value }) => (
)} <SelectItem value={String(value || "")} key={`api-key-expiration-${label}`}>
/> {label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{/* <Accordion
type="multiple"
className="w-full"
>
<AccordionItem value="section-1">
<AccordionTrigger>Scopes</AccordionTrigger>
<AccordionContent>Description of Section 1</AccordionContent>
</AccordionItem>
</Accordion> */}
{/* <h3 className="text-mineshaft-400 text-sm mb-2">Temporariness</h3>
<Switch
id={`enable-ephemerality`}
onCheckedChange={(value) => setIsTemporary(value)}
isChecked={isTemporary}
>
<div className="w-96 mr-4">
<p className="text-gray-400 text-md">This token will be deactivated after your specified duration.</p>
</div>
</Switch>
{isTemporary && (
<Controller
control={control}
name="expiresIn"
defaultValue="15552000"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Duration"
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>
)}
/>
)} */}
<div className="mt-8 flex items-center"> <div className="mt-8 flex items-center">
<Button <Button
className="mr-4" className="mr-4"
size="sm" size="sm"
type="submit" type="submit"
// isLoading={isLoading} isLoading={isSubmitting}
// isDisabled={isLoading} isDisabled={isSubmitting}
> >
Create Create
</Button> </Button>
@@ -1,14 +1,47 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AddServiceTokenV3Modal } from "./AddServiceTokenV3Modal";
import { ServiceTokenV3Table } from "./ServiceTokenV3Table"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button } from "@app/components/v2"; import {
Button,
DeleteActionModal
} from "@app/components/v2";
import {
useDeleteServiceTokenV3
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { AddServiceTokenV3Modal } from "./AddServiceTokenV3Modal";
import { ServiceTokenV3Table } from "./ServiceTokenV3Table";
export const ServiceTokenV3Section = () => { export const ServiceTokenV3Section = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ const { createNotification } = useNotificationContext();
"createServiceTokenV3" const { mutateAsync: deleteMutateAsync } = useDeleteServiceTokenV3();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"serviceTokenV3",
"deleteServiceTokenV3"
] as const); ] 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 ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between mb-8"> <div className="flex justify-between mb-8">
@@ -19,16 +52,31 @@ export const ServiceTokenV3Section = () => {
colorSchema="secondary" colorSchema="secondary"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("createServiceTokenV3")} onClick={() => handlePopUpOpen("serviceTokenV3")}
> >
Create ST V3 Create ST V3
</Button> </Button>
</div> </div>
<ServiceTokenV3Table /> <ServiceTokenV3Table
handlePopUpOpen={handlePopUpOpen}
/>
<AddServiceTokenV3Modal <AddServiceTokenV3Modal
popUp={popUp} popUp={popUp}
handlePopUpToggle={handlePopUpToggle} 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> </div>
); );
} }
@@ -1,6 +1,6 @@
import { faKey, faPencil,faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 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 { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import {
EmptyState, EmptyState,
@@ -15,38 +15,34 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { import {
useGetWorkspaceServiceTokenDataV3, useGetWorkspaceServiceTokenDataV3,
useUpdateServiceTokenV3, useUpdateServiceTokenV3
useDeleteServiceTokenV3
} from "@app/hooks/api"; } 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 { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetWorkspaceServiceTokenDataV3(currentWorkspace?._id || ""); const { data, isLoading } = useGetWorkspaceServiceTokenDataV3(currentWorkspace?._id || "");
const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3(); 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 ({ const handleToggleServiceTokenDataStatus = async ({
serviceTokenDataId, serviceTokenDataId,
@@ -92,10 +88,11 @@ export const ServiceTokenV3Table = () => {
<Tr> <Tr>
<Th>Name</Th> <Th>Name</Th>
<Th>Status</Th> <Th>Status</Th>
<Th>Last Active</Th> <Th>Scopes</Th>
<Th>Created</Th> <Th>Last Used</Th>
<Th>Expiration</Th> <Th>Created At</Th>
<Th className="w-5"></Th> <Th>Expires At</Th>
<Th className="w-5" />
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
@@ -108,32 +105,48 @@ export const ServiceTokenV3Table = () => {
name, name,
isActive, isActive,
lastUsed, lastUsed,
scopes,
createdAt, createdAt,
// expiresAt expiresAt
}) => { }) => {
return ( return (
<Tr className="h-10" key={`st-v3-${_id}`}> <Tr className="h-10" key={`st-v3-${_id}`}>
<Td>{name}</Td> <Td>{name}</Td>
<Td> <Td>
<Switch <Switch
id="test" id={`enable-service-token-${_id}`}
// id={`enable-${authMethodOpt.value}-auth`} onCheckedChange={(value) => handleToggleServiceTokenDataStatus({
onCheckedChange={(value) => handleToggleServiceTokenDataStatus({ serviceTokenDataId: _id,
serviceTokenDataId: _id, isActive: value
isActive: value })}
isChecked={isActive}
>
<p className="w-12 mr-4">{isActive ? "Active" : "Inactive"}</p>
</Switch>
</Td>
<Td>
{scopes.map((scope) => {
return (
<p key={`service-token-${_id}-scope-${scope.environment}-${scope.secretPath}`}>
<span className="font-bold">
{scope.permission}
</span>
{` @${scope.environment} - ${scope.secretPath}`}
</p>
);
})} })}
isChecked={isActive}
>
<p className="w-12 mr-4">{isActive ? "Active" : "Inactive"}</p>
</Switch>
</Td> </Td>
<Td>{lastUsed ? formatDate(lastUsed) : "-"}</Td> <Td>{lastUsed ? formatDate(lastUsed) : "-"}</Td>
<Td>{formatDate(createdAt)}</Td> <Td>{formatDate(createdAt)}</Td>
<Td>{formatDate(createdAt)}</Td> <Td>{expiresAt ? formatDate(expiresAt) : "-"}</Td>
<Td className="flex justify-end"> <Td className="flex justify-end">
<IconButton <IconButton
onClick={async () => { onClick={async () => {
console.log("edit"); handlePopUpOpen("serviceTokenV3", {
serviceTokenDataId: _id,
name,
scopes,
});
}} }}
size="lg" size="lg"
colorSchema="primary" colorSchema="primary"
@@ -143,7 +156,12 @@ export const ServiceTokenV3Table = () => {
<FontAwesomeIcon icon={faPencil} /> <FontAwesomeIcon icon={faPencil} />
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => handleDeleteServiceTokenData(_id)} onClick={() => {
handlePopUpOpen("deleteServiceTokenV3", {
serviceTokenDataId: _id,
name
});
}}
size="lg" size="lg"
colorSchema="danger" colorSchema="danger"
variant="plain" variant="plain"