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

View File

@@ -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

View File

@@ -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 }
: {}
)
});
};

View File

@@ -28,4 +28,5 @@ export * from "./apiKeyData";
export * from "./loginSRPDetail";
export * from "./tokenVersion";
export * from "./webhooks";
export * from "./serviceTokenDataV3";
export * from "./serviceTokenDataV3";
export * from "./serviceTokenDataV3Key";

View File

@@ -1,3 +1,4 @@
// TODO: deprecate
import { Schema, Types, model } from "mongoose";
export interface IServiceToken {
_id: Types.ObjectId;

View File

@@ -1,3 +1,4 @@
// TODO: deprecate
import { Document, Schema, Types, model } from "mongoose";
export interface IServiceTokenData extends Document {

View File

@@ -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<Scope>;
}
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
}
},
{

View File

@@ -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);

View File

@@ -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({

View File

@@ -29,7 +29,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
<Card
isRounded
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
)}
>

View File

@@ -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<DeleteServiceTokenRes, {}, string>({
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));
}
});

View File

@@ -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 = {

View File

@@ -88,7 +88,10 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
} = useForm<FormData>({
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,

View File

@@ -43,7 +43,7 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
<THead>
<Tr>
<Th>Token Name</Th>
<Th>Envrionment - Secret Path</Th>
<Th>Environment - Secret Path</Th>
<Th>Valid Until</Th>
<Th aria-label="button" />
</Tr>

View File

@@ -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<typeof schema>;
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<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 ({
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 (
<Modal
isOpen={popUp?.createServiceTokenV3?.isOpen}
isOpen={popUp?.serviceTokenV3?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("createServiceTokenV3", isOpen);
handlePopUpToggle("serviceTokenV3", isOpen);
reset();
}}
>
<ModalContent title="Create Service Token V3">
<ModalContent title={`${popUp?.serviceTokenV3?.data ? "Update" : "Create"} Service Token V3`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
@@ -139,38 +241,184 @@ export const AddServiceTokenV3Modal = ({
</FormControl>
)}
/>
<Controller
control={control}
name="expiresIn"
defaultValue="15552000"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Expiration"
errorText={error?.message}
isError={Boolean(error)}
{tokenScopes.map(({ id }, index) => (
<div className="flex items-end space-x-2 mb-3" key={id}>
<Controller
control={control}
name={`scopes.${index}.permission`}
defaultValue={currentWorkspace?.environments?.[0]?.slug}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
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
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
Add Scope
</Button>
</div>
<Controller
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 }) => (
<SelectItem value={String(value || "")} key={`api-key-expiration-${label}`}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<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>
)}
/>
{/* <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">
<Button
className="mr-4"
size="sm"
type="submit"
// isLoading={isLoading}
// isDisabled={isLoading}
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
</Button>

View File

@@ -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 (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between mb-8">
@@ -19,16 +52,31 @@ export const ServiceTokenV3Section = () => {
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("createServiceTokenV3")}
onClick={() => handlePopUpOpen("serviceTokenV3")}
>
Create ST V3
</Button>
</div>
<ServiceTokenV3Table />
<ServiceTokenV3Table
handlePopUpOpen={handlePopUpOpen}
/>
<AddServiceTokenV3Modal
popUp={popUp}
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>
);
}

View File

@@ -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 = () => {
<Tr>
<Th>Name</Th>
<Th>Status</Th>
<Th>Last Active</Th>
<Th>Created</Th>
<Th>Expiration</Th>
<Th className="w-5"></Th>
<Th>Scopes</Th>
<Th>Last Used</Th>
<Th>Created At</Th>
<Th>Expires At</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
@@ -108,32 +105,48 @@ export const ServiceTokenV3Table = () => {
name,
isActive,
lastUsed,
scopes,
createdAt,
// expiresAt
expiresAt
}) => {
return (
<Tr className="h-10" key={`st-v3-${_id}`}>
<Td>{name}</Td>
<Td>
<Switch
id="test"
// id={`enable-${authMethodOpt.value}-auth`}
onCheckedChange={(value) => handleToggleServiceTokenDataStatus({
serviceTokenDataId: _id,
isActive: value
})}
isChecked={isActive}
>
<p className="w-12 mr-4">{isActive ? "Active" : "Inactive"}</p>
</Switch>
<Switch
id={`enable-service-token-${_id}`}
onCheckedChange={(value) => handleToggleServiceTokenDataStatus({
serviceTokenDataId: _id,
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>
);
})}
</Td>
<Td>{lastUsed ? formatDate(lastUsed) : "-"}</Td>
<Td>{formatDate(createdAt)}</Td>
<Td>{formatDate(createdAt)}</Td>
<Td>{expiresAt ? formatDate(expiresAt) : "-"}</Td>
<Td className="flex justify-end">
<IconButton
onClick={async () => {
console.log("edit");
handlePopUpOpen("serviceTokenV3", {
serviceTokenDataId: _id,
name,
scopes,
});
}}
size="lg"
colorSchema="primary"
@@ -143,7 +156,12 @@ export const ServiceTokenV3Table = () => {
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
onClick={() => handleDeleteServiceTokenData(_id)}
onClick={() => {
handlePopUpOpen("deleteServiceTokenV3", {
serviceTokenDataId: _id,
name
});
}}
size="lg"
colorSchema="danger"
variant="plain"