mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Finish preliminary CRUD ops for service token v3, ServiceTokenV3Key structure
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
: {}
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// TODO: deprecate
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
export interface IServiceToken {
|
||||
_id: Types.ObjectId;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// TODO: deprecate
|
||||
import { Document, Schema, Types, model } from "mongoose";
|
||||
|
||||
export interface IServiceTokenData extends Document {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
43
backend/src/models/serviceTokenDataV3Key.ts
Normal file
43
backend/src/models/serviceTokenDataV3Key.ts
Normal 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);
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user