From 1896442168fa90b517d1d6734ecc2bea86f95b70 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 20 Sep 2023 17:32:33 +0100 Subject: [PATCH] Finish basic scaffolding for service token v3 --- backend/src/controllers/v3/index.ts | 2 + .../v3/serviceTokenDataController.ts | 70 +++++++ .../controllers/v3/workspacesController.ts | 16 +- backend/src/ee/routes/v1/sso.ts | 4 +- backend/src/index.ts | 7 +- backend/src/models/index.ts | 15 +- ...erviceTokenV3.ts => serviceTokenDataV3.ts} | 19 +- backend/src/routes/v3/index.ts | 2 + backend/src/routes/v3/serviceTokenData.ts | 31 +++ backend/src/routes/v3/workspaces.ts | 8 + backend/src/utils/auth.ts | 6 +- backend/src/validation/index.ts | 1 + backend/src/validation/serviceTokenV3.ts | 25 +++ backend/src/validation/workspace.ts | 6 + frontend/src/hooks/api/serviceTokens/index.ts | 9 +- .../src/hooks/api/serviceTokens/queries.tsx | 63 +++++- frontend/src/hooks/api/serviceTokens/types.ts | 35 ++++ frontend/src/hooks/api/workspace/index.tsx | 4 +- frontend/src/hooks/api/workspace/queries.tsx | 20 +- .../APIKeySection/APIKeyTable.tsx | 96 +++++---- .../APIKeySection/AddAPIKeyModal.tsx | 24 +-- .../ProjectSettingsPage.tsx | 11 +- .../AddServiceTokenV3Modal.tsx | 186 +++++++++++++++++- .../ServiceTokenV3Section.tsx | 28 ++- .../ServiceTokenV3Table.tsx | 169 +++++++++++++++- 25 files changed, 756 insertions(+), 101 deletions(-) create mode 100644 backend/src/controllers/v3/serviceTokenDataController.ts rename backend/src/models/{serviceTokenV3.ts => serviceTokenDataV3.ts} (50%) create mode 100644 backend/src/routes/v3/serviceTokenData.ts create mode 100644 backend/src/validation/serviceTokenV3.ts diff --git a/backend/src/controllers/v3/index.ts b/backend/src/controllers/v3/index.ts index 959bab532..fa853d8c5 100644 --- a/backend/src/controllers/v3/index.ts +++ b/backend/src/controllers/v3/index.ts @@ -2,10 +2,12 @@ import * as secretsController from "./secretsController"; import * as workspacesController from "./workspacesController"; import * as authController from "./authController"; import * as signupController from "./signupController"; +import * as serviceTokenDataController from "./serviceTokenDataController"; export { authController, secretsController, signupController, workspacesController, + serviceTokenDataController } diff --git a/backend/src/controllers/v3/serviceTokenDataController.ts b/backend/src/controllers/v3/serviceTokenDataController.ts new file mode 100644 index 000000000..61ce1f6b3 --- /dev/null +++ b/backend/src/controllers/v3/serviceTokenDataController.ts @@ -0,0 +1,70 @@ +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { ServiceTokenDataV3 } from "../../models"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/serviceTokenV3"; +import { createToken } from "../../helpers/auth"; + +export const createServiceTokenData = async (req: Request, res: Response) => { + const { + body: { name, workspaceId, publicKey } + } = await validateRequest(reqValidator.CreateServiceTokenV3, req); + + const serviceTokenData = await new ServiceTokenDataV3({ + name, + workspace: new Types.ObjectId(workspaceId), + publicKey, + isActive: false + }).save(); + + console.log("the newly created serviceTokenDataV3: ", serviceTokenData); + + const token = createToken({ + payload: { + _id: serviceTokenData._id.toString() + }, + expiresIn: "5d", + secret: "hello" // TODO: replace with real secret + }); + + console.log("jwt token: ", token); + + return res.status(200).send({ + serviceTokenData, + serviceToken: `proj_token.${token}` + }); +} + +export const updateServiceTokenData = async (req: Request, res: Response) => { + const { + params: { serviceTokenDataId }, + body: { name, isActive } + } = await validateRequest(reqValidator.UpdateServiceTokenV3, req); + + const serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate( + serviceTokenDataId, + { + name, + isActive + }, + { + new: true + } + ); + + return res.status(200).send({ + serviceTokenData + }); +} + +export const deleteServiceTokenData = async (req: Request, res: Response) => { + const { + params: { serviceTokenDataId } + } = await validateRequest(reqValidator.DeleteServiceTokenV3, req); + + const serviceTokenData = await ServiceTokenDataV3.findByIdAndDelete(serviceTokenDataId); + + return res.status(200).send({ + serviceTokenData + }); +} \ No newline at end of file diff --git a/backend/src/controllers/v3/workspacesController.ts b/backend/src/controllers/v3/workspacesController.ts index 8469cdb5e..f1edca1f7 100644 --- a/backend/src/controllers/v3/workspacesController.ts +++ b/backend/src/controllers/v3/workspacesController.ts @@ -1,7 +1,7 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { validateRequest } from "../../helpers/validation"; -import { Secret } from "../../models"; +import { Secret, ServiceTokenDataV3 } from "../../models"; import { SecretService } from "../../services"; import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService"; import { UnauthorizedRequestError } from "../../utils/errors"; @@ -101,3 +101,17 @@ export const nameWorkspaceSecrets = async (req: Request, res: Response) => { message: "Successfully named workspace secrets" }); }; + +export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => { + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceServiceTokenDataV3, req); + + const serviceTokenData = await ServiceTokenDataV3.find({ + workspace: new Types.ObjectId(workspaceId) + }); + + return res.status(200).send({ + serviceTokenData + }); +} \ No newline at end of file diff --git a/backend/src/ee/routes/v1/sso.ts b/backend/src/ee/routes/v1/sso.ts index 564073aec..90d25cd45 100644 --- a/backend/src/ee/routes/v1/sso.ts +++ b/backend/src/ee/routes/v1/sso.ts @@ -30,6 +30,7 @@ router.get( router.get("/redirect/github", authLimiter, (req, res, next) => { passport.authenticate("github", { session: false, + scope: [ 'user:email' ], ...(req.query.callback_port ? { state: req.query.callback_port as string @@ -43,7 +44,8 @@ router.get( authLimiter, passport.authenticate("github", { failureRedirect: "/login/provider/error", - session: false + session: false, + scope: [ 'user:email' ] }), ssoController.redirectSSO ); diff --git a/backend/src/index.ts b/backend/src/index.ts index aa4440a55..cb4062bcc 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -65,7 +65,8 @@ import { auth as v3AuthRouter, secrets as v3SecretsRouter, signup as v3SignupRouter, - workspaces as v3WorkspacesRouter + workspaces as v3WorkspacesRouter, + serviceTokenData as v3ServiceTokenDataRouter } from "./routes/v3"; import { healthCheck } from "./routes/status"; import { getLogger } from "./utils/logger"; @@ -188,13 +189,15 @@ const main = async () => { app.use("/api/v2/secret", v2SecretRouter); // deprecate app.use("/api/v2/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets app.use("/api/v2/service-token", v2ServiceTokenDataRouter); - app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new + // app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new // v3 routes (experimental) app.use("/api/v3/auth", v3AuthRouter); app.use("/api/v3/secrets", v3SecretsRouter); app.use("/api/v3/workspaces", v3WorkspacesRouter); app.use("/api/v3/signup", v3SignupRouter); + app.use("/api/v3/service-token", v3ServiceTokenDataRouter); + // api docs app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile)); diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 7a431592b..40fa34d6e 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -14,17 +14,18 @@ export * from "./tag"; export * from "./folder"; export * from "./secretImports"; export * from "./secretBlindIndexData"; -export * from "./serviceToken"; -export * from "./serviceAccount"; -export * from "./serviceAccountKey"; -export * from "./serviceAccountOrganizationPermission"; -export * from "./serviceAccountWorkspacePermission"; +export * from "./serviceToken"; // TODO: deprecate +export * from "./serviceAccount"; // TODO: deprecate +export * from "./serviceAccountKey"; // TODO: deprecate +export * from "./serviceAccountOrganizationPermission"; // TODO: deprecate +export * from "./serviceAccountWorkspacePermission"; // TODO: deprecate export * from "./tokenData"; export * from "./user"; export * from "./userAction"; export * from "./workspace"; -export * from "./serviceTokenData"; +export * from "./serviceTokenData"; // TODO: deprecate export * from "./apiKeyData"; export * from "./loginSRPDetail"; export * from "./tokenVersion"; -export * from "./webhooks"; \ No newline at end of file +export * from "./webhooks"; +export * from "./serviceTokenDataV3"; \ No newline at end of file diff --git a/backend/src/models/serviceTokenV3.ts b/backend/src/models/serviceTokenDataV3.ts similarity index 50% rename from backend/src/models/serviceTokenV3.ts rename to backend/src/models/serviceTokenDataV3.ts index 695b3f894..ee3b733a9 100644 --- a/backend/src/models/serviceTokenV3.ts +++ b/backend/src/models/serviceTokenDataV3.ts @@ -1,13 +1,15 @@ import { Document, Schema, Types, model } from "mongoose"; -export interface IServiceTokenV3 extends Document { +export interface IServiceTokenDataV3 extends Document { _id: Types.ObjectId; name: string; workspace: Types.ObjectId; publicKey: string; + isActive: boolean; + lastUsed: Date; } -const serviceTokenV3Schema = new Schema( +const serviceTokenDataV3Schema = new Schema( { name: { type: String, @@ -21,8 +23,19 @@ const serviceTokenV3Schema = new Schema( publicKey: { type: String, required: true + }, + isActive: { + type: Boolean, + required: true + }, + lastUsed: { + type: Date, + required: false } + }, + { + timestamps: true } ); -export const ServiceTokenV3 = model("ServiceTokenV3", serviceTokenV3Schema); \ No newline at end of file +export const ServiceTokenDataV3 = model("ServiceTokenDataV3", serviceTokenDataV3Schema); \ No newline at end of file diff --git a/backend/src/routes/v3/index.ts b/backend/src/routes/v3/index.ts index f4fcfe55b..c122fc6ed 100644 --- a/backend/src/routes/v3/index.ts +++ b/backend/src/routes/v3/index.ts @@ -2,10 +2,12 @@ import auth from "./auth"; import secrets from "./secrets"; import workspaces from "./workspaces"; import signup from "./signup"; +import serviceTokenData from "./serviceTokenData"; export { auth, secrets, signup, workspaces, + serviceTokenData } diff --git a/backend/src/routes/v3/serviceTokenData.ts b/backend/src/routes/v3/serviceTokenData.ts new file mode 100644 index 000000000..982a1ecaa --- /dev/null +++ b/backend/src/routes/v3/serviceTokenData.ts @@ -0,0 +1,31 @@ +import express from "express"; +const router = express.Router(); +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; +import { serviceTokenDataController } from "../../controllers/v3"; + +router.post( + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + serviceTokenDataController.createServiceTokenData +); + +router.patch( + "/:serviceTokenDataId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + serviceTokenDataController.updateServiceTokenData +); + +router.delete( + "/:serviceTokenDataId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + serviceTokenDataController.deleteServiceTokenData +); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/v3/workspaces.ts b/backend/src/routes/v3/workspaces.ts index 834d54cd1..dcae733fc 100644 --- a/backend/src/routes/v3/workspaces.ts +++ b/backend/src/routes/v3/workspaces.ts @@ -34,4 +34,12 @@ router.post( // -- +router.get( + "/:workspaceId/service-token", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspacesController.getWorkspaceServiceTokenData +); + export default router; diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index f7a1a504c..922e9dc85 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -24,6 +24,8 @@ import { InternalServerError, OrganizationNotFoundError } from "./errors"; import { ACCEPTED, INVITED, MEMBER } from "../variables"; import { getSiteURL } from "../config"; +import { standardRequest } from "../config/request"; + // eslint-disable-next-line @typescript-eslint/no-var-requires const GoogleStrategy = require("passport-google-oauth20").Strategy; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -143,9 +145,11 @@ const initializePassport = async () => { passReqToCallback: true, clientID: clientIdGitHubLogin, clientSecret: clientSecretGitHubLogin, - callbackURL: "/api/v1/sso/github" + callbackURL: "/api/v1/sso/github", + scope: [ 'user:email' ] }, async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => { + const email = profile.emails[0].value; let user = await User.findOne({ diff --git a/backend/src/validation/index.ts b/backend/src/validation/index.ts index 409b563c6..9f20d6741 100644 --- a/backend/src/validation/index.ts +++ b/backend/src/validation/index.ts @@ -9,3 +9,4 @@ export * from "./organization"; export * from "./secrets"; export * from "./serviceAccount"; export * from "./serviceTokenData"; +export * from "./serviceTokenV3"; diff --git a/backend/src/validation/serviceTokenV3.ts b/backend/src/validation/serviceTokenV3.ts new file mode 100644 index 000000000..2f8cba9f0 --- /dev/null +++ b/backend/src/validation/serviceTokenV3.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const CreateServiceTokenV3 = z.object({ + body: z.object({ + name: z.string().trim(), + workspaceId: z.string().trim(), + publicKey: z.string().trim(), + }) +}); + +export const UpdateServiceTokenV3 = z.object({ + params: z.object({ + serviceTokenDataId: z.string() + }), + body: z.object({ + name: z.string().trim().optional(), + isActive: z.boolean().optional() + }) +}); + +export const DeleteServiceTokenV3 = z.object({ + params: z.object({ + serviceTokenDataId: z.string() + }), +}); \ No newline at end of file diff --git a/backend/src/validation/workspace.ts b/backend/src/validation/workspace.ts index 44044d923..b2d41cff8 100644 --- a/backend/src/validation/workspace.ts +++ b/backend/src/validation/workspace.ts @@ -299,3 +299,9 @@ export const NameWorkspaceSecretsV3 = z.object({ .array() }) }); + +export const GetWorkspaceServiceTokenDataV3 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); \ No newline at end of file diff --git a/frontend/src/hooks/api/serviceTokens/index.ts b/frontend/src/hooks/api/serviceTokens/index.ts index b594d5a64..5d55c22c4 100644 --- a/frontend/src/hooks/api/serviceTokens/index.ts +++ b/frontend/src/hooks/api/serviceTokens/index.ts @@ -1 +1,8 @@ -export { useCreateServiceToken, useDeleteServiceToken, useGetUserWsServiceTokens } from "./queries"; +export { + useCreateServiceToken, + useDeleteServiceToken, + useGetUserWsServiceTokens, + useCreateServiceTokenV3, + useUpdateServiceTokenV3, + useDeleteServiceTokenV3 +} from "./queries"; diff --git a/frontend/src/hooks/api/serviceTokens/queries.tsx b/frontend/src/hooks/api/serviceTokens/queries.tsx index 4a0241cdf..eda5696be 100644 --- a/frontend/src/hooks/api/serviceTokens/queries.tsx +++ b/frontend/src/hooks/api/serviceTokens/queries.tsx @@ -6,8 +6,14 @@ import { CreateServiceTokenDTO, CreateServiceTokenRes, DeleteServiceTokenRes, - ServiceToken + ServiceToken, + ServiceTokenDataV3, + CreateServiceTokenDataV3DTO, + CreateServiceTokenDataV3Res, + UpdateServiceTokenDataV3DTO, + DeleteServiceTokenDataV3DTO } from "./types"; +import { workspaceKeys } from "../workspace/queries"; const serviceTokenKeys = { getAllWorkspaceServiceToken: (workspaceID: string) => [{ workspaceID }, "service-tokens"] as const @@ -32,12 +38,11 @@ export const useGetUserWsServiceTokens = ({ workspaceID }: UseGetWorkspaceServic } // mutation -export const useCreateServiceToken = () => { +export const useCreateServiceToken = () => { // TODO: deprecate const queryClient = useQueryClient(); return useMutation({ mutationFn: async (body) => { - console.log("useCreateServiceToken"); const { data } = await apiRequest.post("/api/v2/service-token/", body); data.serviceToken += `.${body.randomBytes}`; return data; @@ -62,3 +67,55 @@ export const useDeleteServiceToken = () => { } }); }; + +export const useCreateServiceTokenV3 = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (body) => { + const { data } = await apiRequest.post("/api/v3/service-token/", body); + return data; + }, + onSuccess: ({ serviceTokenData }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceServiceTokenDataV3(serviceTokenData.workspace)); + } + }); +}; + +export const useUpdateServiceTokenV3 = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + serviceTokenDataId, + name, + isActive + }) => { + const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, { + name, + isActive + }); + + return serviceTokenData; + }, + onSuccess: ({ workspace }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceServiceTokenDataV3(workspace)); + } + }); +}; + +export const useDeleteServiceTokenV3 = () => { + const queryClient = useQueryClient(); + return useMutation({ + 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)); + } + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/serviceTokens/types.ts b/frontend/src/hooks/api/serviceTokens/types.ts index d6dc322cc..bb88d18fe 100644 --- a/frontend/src/hooks/api/serviceTokens/types.ts +++ b/frontend/src/hooks/api/serviceTokens/types.ts @@ -33,3 +33,38 @@ export type CreateServiceTokenRes = { }; export type DeleteServiceTokenRes = { serviceTokenData: ServiceToken }; + +// --- v3 + +export type ServiceTokenDataV3 = { + _id: string; + name: string; + workspace: string; + isActive: boolean; + lastUsed?: string; + createdAt: string; + updatedAt: string; +}; + +// TODO: add scopes +// TODO: encrypted key info +export type CreateServiceTokenDataV3DTO = { + name: string; + workspaceId: string; + publicKey: string; +} + +export type CreateServiceTokenDataV3Res = { + serviceToken: string; + serviceTokenData: ServiceTokenDataV3; +} + +export type UpdateServiceTokenDataV3DTO = { + serviceTokenDataId: string; + name?: string; + isActive?: boolean; +} + +export type DeleteServiceTokenDataV3DTO = { + serviceTokenDataId: string; +} \ No newline at end of file diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index fe63c2ad2..e2dd3105a 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -19,4 +19,6 @@ export { useReorderWsEnvironment, useToggleAutoCapitalization, useUpdateUserWorkspaceRole, - useUpdateWsEnvironment} from "./queries"; + useUpdateWsEnvironment, + useGetWorkspaceServiceTokenDataV3 +} from "./queries"; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index f9e40d2db..b5abe7827 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -6,6 +6,7 @@ import { IntegrationAuth } from "../integrationAuth/types"; import { TIntegration } from "../integrations/types"; import { EncryptedSecret } from "../secrets/types"; import { TWorkspaceUser } from "../users/types"; +import { ServiceTokenDataV3 } from "../serviceTokens/types"; import { CreateEnvironmentDTO, CreateWorkspaceDTO, @@ -32,7 +33,8 @@ export const workspaceKeys = { getAllUserWorkspace: ["workspaces"] as const, getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const, getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const, - getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const + getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const, + getWorkspaceServiceTokenDataV3: (workspaceId: string) => [{ workspaceId }, "workspace-service-token-data-v3"] as const }; const fetchWorkspaceById = async (workspaceId: string) => { @@ -361,3 +363,19 @@ export const useUpdateUserWorkspaceRole = () => { } }); }; + +export const useGetWorkspaceServiceTokenDataV3 = (workspaceId: string) => { + return useQuery({ + queryKey: workspaceKeys.getWorkspaceServiceTokenDataV3(workspaceId), + queryFn: async () => { + const { + data: { serviceTokenData } + } = await apiRequest.get<{ serviceTokenData: ServiceTokenDataV3[] }>( + `/api/v3/workspaces/${workspaceId}/service-token` + ); + + return serviceTokenData; + }, + enabled: true + }); +}; \ No newline at end of file diff --git a/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/APIKeyTable.tsx b/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/APIKeyTable.tsx index 40a309eb0..788b8d23c 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/APIKeyTable.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/APIKeyTable.tsx @@ -49,56 +49,54 @@ export const APIKeyTable = () => { }; return ( -
- - - + +
+ + + + + + + + + + {isLoading && } + {!isLoading && + data && + data.length > 0 && + data.map(({ _id, name, createdAt, expiresAt, lastUsed }) => { + return ( + + + + + + + + ); + })} + {!isLoading && data && data?.length === 0 && ( - - - - - - - - {isLoading && } - {!isLoading && - data && - data.length > 0 && - data.map(({ _id, name, createdAt, expiresAt, lastUsed }) => { - return ( - - - - - - - - ); - })} - {!isLoading && data && data?.length === 0 && ( - - - - )} - -
NameLast activeCreatedExpiration +
{name}{formatDate(lastUsed)}{formatDate(createdAt)}{formatDate(expiresAt)} + { + await handleDeleteAPIKeyDataClick(_id); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + > + + +
NameLast activeCreatedExpiration + + +
{name}{formatDate(lastUsed)}{formatDate(createdAt)}{formatDate(expiresAt)} - { - await handleDeleteAPIKeyDataClick(_id); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - > - - -
- -
-
-
+ )} + + + ); }; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/AddAPIKeyModal.tsx b/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/AddAPIKeyModal.tsx index ef6008a67..e3b43f8ae 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/AddAPIKeyModal.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/AddAPIKeyModal.tsx @@ -161,18 +161,18 @@ export const AddAPIKeyModal = ({ )} />
- - + +
) : ( diff --git a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx index b70760d7f..8aa06594b 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx @@ -2,8 +2,6 @@ import { Fragment } from "react"; import { useTranslation } from "react-i18next"; import { Tab } from "@headlessui/react"; -import NavHeader from "@app/components/navigation/NavHeader"; - import { ProjectGeneralTab } from "./components/ProjectGeneralTab"; import { ProjectServiceTokensTab } from "./components/ProjectServiceTokensTab"; import { WebhooksTab } from "./components/WebhooksTab"; @@ -17,12 +15,9 @@ const tabs = [ export const ProjectSettingsPage = () => { const { t } = useTranslation(); return ( -
-
-
- -
-
+
+
+

{t("settings.project.title")}

diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx index 6fd6ce0d2..f3eebb2d6 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx @@ -1,7 +1,185 @@ -export const AddServiceTokenV3Modal = () => { +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 { + Modal, + ModalContent, + FormControl, + Select, + SelectItem, + Input, + Button +} 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"; + +const expirations = [ + { label: "1 day", value: "86400" }, + { label: "7 days", value: "604800" }, + { label: "1 month", value: "2592000" }, + { label: "6 months", value: "15552000" }, + { label: "12 months", value: "31104000" } +]; + +const schema = yup.object({ + name: yup.string().required("ST V3 name is required"), + expiresIn: yup.string().required("ST V3 expiration window is required") +}).required(); + +export type FormData = yup.InferType; + +type Props = { + popUp: UsePopUpState<["createServiceTokenV3"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["createServiceTokenV3"]>, 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 { createNotification } = useNotificationContext(); + const { + control, + handleSubmit, + reset + } = useForm({ + resolver: yupResolver(schema) + }); + + const onFormSubmit = async ({ + name, + expiresIn + }: 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 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", + type: "success" + }); + + reset(); + handlePopUpToggle("createServiceTokenV3", false); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to create ST V3", + type: "error" + }); + } + } + return ( -
- AddServiceTokenV3Modal -
+ { + handlePopUpToggle("createServiceTokenV3", isOpen); + reset(); + }} + > + +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ +
+
); } \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx index 4d8250fd3..9ca14ef9c 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Section.tsx @@ -1,14 +1,34 @@ +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 { usePopUp } from "@app/hooks/usePopUp"; export const ServiceTokenV3Section = () => { + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "createServiceTokenV3" + ] as const); return (
-

- Service Tokens 2.0 -

+
+

+ Service Tokens 2.0 +

+ +
- +
); } \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx index 77b2b8fac..66a4c7025 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx @@ -1,7 +1,170 @@ +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, + IconButton, + Switch, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { + useGetWorkspaceServiceTokenDataV3, + useUpdateServiceTokenV3, + useDeleteServiceTokenV3 +} from "@app/hooks/api"; + export const ServiceTokenV3Table = () => { + 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, + isActive + }: { + serviceTokenDataId: string; + isActive: boolean; + }) => { + try { + await updateMutateAsync({ + serviceTokenDataId, + isActive + }); + + createNotification({ + text: `Successfully ${isActive ? "enabled" : "disabled"} service token v3`, + type: "success" + }); + } catch (err) { + console.log(err); + createNotification({ + text: `Failed to ${isActive ? "enable" : "disable"} service token v3`, + type: "error" + }); + } + } + + const formatDate = (dateToFormat: string) => { + const date = new Date(dateToFormat); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + + const formattedDate = `${day}/${month}/${year}`; + + return formattedDate; + }; + return ( -
- ServiceTokenV3Table -
+ + + + + + + + + + + + + + {isLoading && } + {!isLoading && + data && + data.length > 0 && + data.map(({ + _id, + name, + isActive, + lastUsed, + createdAt, + // expiresAt + }) => { + return ( + + + + + + + + + ); + })} + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
NameStatusLast ActiveCreatedExpiration
{name} + handleToggleServiceTokenDataStatus({ + serviceTokenDataId: _id, + isActive: value + })} + isChecked={isActive} + > +

{isActive ? "Active" : "Inactive"}

+
+
{lastUsed ? formatDate(lastUsed) : "-"}{formatDate(createdAt)}{formatDate(createdAt)} + { + console.log("edit"); + }} + size="lg" + colorSchema="primary" + variant="plain" + ariaLabel="update" + > + + + handleDeleteServiceTokenData(_id)} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="ml-4" + > + + +
+ +
+
); } \ No newline at end of file