Finish basic scaffolding for service token v3

This commit is contained in:
Tuan Dang
2023-09-20 17:32:33 +01:00
parent 1cdd840485
commit 1896442168
25 changed files with 756 additions and 101 deletions
+2
View File
@@ -2,10 +2,12 @@ import * as secretsController from "./secretsController";
import * as workspacesController from "./workspacesController"; import * as workspacesController from "./workspacesController";
import * as authController from "./authController"; import * as authController from "./authController";
import * as signupController from "./signupController"; import * as signupController from "./signupController";
import * as serviceTokenDataController from "./serviceTokenDataController";
export { export {
authController, authController,
secretsController, secretsController,
signupController, signupController,
workspacesController, workspacesController,
serviceTokenDataController
} }
@@ -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
});
}
@@ -1,7 +1,7 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import { Secret } from "../../models"; import { Secret, ServiceTokenDataV3 } from "../../models";
import { SecretService } from "../../services"; import { SecretService } from "../../services";
import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService"; import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService";
import { UnauthorizedRequestError } from "../../utils/errors"; import { UnauthorizedRequestError } from "../../utils/errors";
@@ -101,3 +101,17 @@ export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
message: "Successfully named workspace secrets" 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
});
}
+3 -1
View File
@@ -30,6 +30,7 @@ router.get(
router.get("/redirect/github", authLimiter, (req, res, next) => { router.get("/redirect/github", authLimiter, (req, res, next) => {
passport.authenticate("github", { passport.authenticate("github", {
session: false, session: false,
scope: [ 'user:email' ],
...(req.query.callback_port ...(req.query.callback_port
? { ? {
state: req.query.callback_port as string state: req.query.callback_port as string
@@ -43,7 +44,8 @@ router.get(
authLimiter, authLimiter,
passport.authenticate("github", { passport.authenticate("github", {
failureRedirect: "/login/provider/error", failureRedirect: "/login/provider/error",
session: false session: false,
scope: [ 'user:email' ]
}), }),
ssoController.redirectSSO ssoController.redirectSSO
); );
+5 -2
View File
@@ -65,7 +65,8 @@ import {
auth as v3AuthRouter, auth as v3AuthRouter,
secrets as v3SecretsRouter, secrets as v3SecretsRouter,
signup as v3SignupRouter, signup as v3SignupRouter,
workspaces as v3WorkspacesRouter workspaces as v3WorkspacesRouter,
serviceTokenData as v3ServiceTokenDataRouter
} from "./routes/v3"; } from "./routes/v3";
import { healthCheck } from "./routes/status"; import { healthCheck } from "./routes/status";
import { getLogger } from "./utils/logger"; import { getLogger } from "./utils/logger";
@@ -188,13 +189,15 @@ const main = async () => {
app.use("/api/v2/secret", v2SecretRouter); // deprecate 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/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets
app.use("/api/v2/service-token", v2ServiceTokenDataRouter); 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) // v3 routes (experimental)
app.use("/api/v3/auth", v3AuthRouter); app.use("/api/v3/auth", v3AuthRouter);
app.use("/api/v3/secrets", v3SecretsRouter); app.use("/api/v3/secrets", v3SecretsRouter);
app.use("/api/v3/workspaces", v3WorkspacesRouter); app.use("/api/v3/workspaces", v3WorkspacesRouter);
app.use("/api/v3/signup", v3SignupRouter); app.use("/api/v3/signup", v3SignupRouter);
app.use("/api/v3/service-token", v3ServiceTokenDataRouter);
// api docs // api docs
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile)); app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile));
+8 -7
View File
@@ -14,17 +14,18 @@ export * from "./tag";
export * from "./folder"; export * from "./folder";
export * from "./secretImports"; export * from "./secretImports";
export * from "./secretBlindIndexData"; export * from "./secretBlindIndexData";
export * from "./serviceToken"; export * from "./serviceToken"; // TODO: deprecate
export * from "./serviceAccount"; export * from "./serviceAccount"; // TODO: deprecate
export * from "./serviceAccountKey"; export * from "./serviceAccountKey"; // TODO: deprecate
export * from "./serviceAccountOrganizationPermission"; export * from "./serviceAccountOrganizationPermission"; // TODO: deprecate
export * from "./serviceAccountWorkspacePermission"; export * from "./serviceAccountWorkspacePermission"; // TODO: deprecate
export * from "./tokenData"; export * from "./tokenData";
export * from "./user"; export * from "./user";
export * from "./userAction"; export * from "./userAction";
export * from "./workspace"; export * from "./workspace";
export * from "./serviceTokenData"; export * from "./serviceTokenData"; // TODO: deprecate
export * from "./apiKeyData"; export * from "./apiKeyData";
export * from "./loginSRPDetail"; export * from "./loginSRPDetail";
export * from "./tokenVersion"; export * from "./tokenVersion";
export * from "./webhooks"; export * from "./webhooks";
export * from "./serviceTokenDataV3";
@@ -1,13 +1,15 @@
import { Document, Schema, Types, model } from "mongoose"; import { Document, Schema, Types, model } from "mongoose";
export interface IServiceTokenV3 extends Document { export interface IServiceTokenDataV3 extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
name: string; name: string;
workspace: Types.ObjectId; workspace: Types.ObjectId;
publicKey: string; publicKey: string;
isActive: boolean;
lastUsed: Date;
} }
const serviceTokenV3Schema = new Schema( const serviceTokenDataV3Schema = new Schema(
{ {
name: { name: {
type: String, type: String,
@@ -21,8 +23,19 @@ const serviceTokenV3Schema = new Schema(
publicKey: { publicKey: {
type: String, type: String,
required: true required: true
},
isActive: {
type: Boolean,
required: true
},
lastUsed: {
type: Date,
required: false
} }
},
{
timestamps: true
} }
); );
export const ServiceTokenV3 = model<IServiceTokenV3>("ServiceTokenV3", serviceTokenV3Schema); export const ServiceTokenDataV3 = model<IServiceTokenDataV3>("ServiceTokenDataV3", serviceTokenDataV3Schema);
+2
View File
@@ -2,10 +2,12 @@ import auth from "./auth";
import secrets from "./secrets"; import secrets from "./secrets";
import workspaces from "./workspaces"; import workspaces from "./workspaces";
import signup from "./signup"; import signup from "./signup";
import serviceTokenData from "./serviceTokenData";
export { export {
auth, auth,
secrets, secrets,
signup, signup,
workspaces, workspaces,
serviceTokenData
} }
+31
View File
@@ -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;
+8
View File
@@ -34,4 +34,12 @@ router.post(
// -- // --
router.get(
"/:workspaceId/service-token",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
workspacesController.getWorkspaceServiceTokenData
);
export default router; export default router;
+5 -1
View File
@@ -24,6 +24,8 @@ import { InternalServerError, OrganizationNotFoundError } from "./errors";
import { ACCEPTED, INVITED, MEMBER } from "../variables"; import { ACCEPTED, INVITED, MEMBER } from "../variables";
import { getSiteURL } from "../config"; import { getSiteURL } from "../config";
import { standardRequest } from "../config/request";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const GoogleStrategy = require("passport-google-oauth20").Strategy; const GoogleStrategy = require("passport-google-oauth20").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -143,9 +145,11 @@ const initializePassport = async () => {
passReqToCallback: true, passReqToCallback: true,
clientID: clientIdGitHubLogin, clientID: clientIdGitHubLogin,
clientSecret: clientSecretGitHubLogin, 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) => { async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => {
const email = profile.emails[0].value; const email = profile.emails[0].value;
let user = await User.findOne({ let user = await User.findOne({
+1
View File
@@ -9,3 +9,4 @@ export * from "./organization";
export * from "./secrets"; export * from "./secrets";
export * from "./serviceAccount"; export * from "./serviceAccount";
export * from "./serviceTokenData"; export * from "./serviceTokenData";
export * from "./serviceTokenV3";
+25
View File
@@ -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()
}),
});
+6
View File
@@ -299,3 +299,9 @@ export const NameWorkspaceSecretsV3 = z.object({
.array() .array()
}) })
}); });
export const GetWorkspaceServiceTokenDataV3 = z.object({
params: z.object({
workspaceId: z.string().trim()
})
});
@@ -1 +1,8 @@
export { useCreateServiceToken, useDeleteServiceToken, useGetUserWsServiceTokens } from "./queries"; export {
useCreateServiceToken,
useDeleteServiceToken,
useGetUserWsServiceTokens,
useCreateServiceTokenV3,
useUpdateServiceTokenV3,
useDeleteServiceTokenV3
} from "./queries";
@@ -6,8 +6,14 @@ import {
CreateServiceTokenDTO, CreateServiceTokenDTO,
CreateServiceTokenRes, CreateServiceTokenRes,
DeleteServiceTokenRes, DeleteServiceTokenRes,
ServiceToken ServiceToken,
ServiceTokenDataV3,
CreateServiceTokenDataV3DTO,
CreateServiceTokenDataV3Res,
UpdateServiceTokenDataV3DTO,
DeleteServiceTokenDataV3DTO
} from "./types"; } from "./types";
import { workspaceKeys } from "../workspace/queries";
const serviceTokenKeys = { const serviceTokenKeys = {
getAllWorkspaceServiceToken: (workspaceID: string) => [{ workspaceID }, "service-tokens"] as const getAllWorkspaceServiceToken: (workspaceID: string) => [{ workspaceID }, "service-tokens"] as const
@@ -32,12 +38,11 @@ export const useGetUserWsServiceTokens = ({ workspaceID }: UseGetWorkspaceServic
} }
// mutation // mutation
export const useCreateServiceToken = () => { export const useCreateServiceToken = () => { // TODO: deprecate
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<CreateServiceTokenRes, {}, CreateServiceTokenDTO>({ return useMutation<CreateServiceTokenRes, {}, CreateServiceTokenDTO>({
mutationFn: async (body) => { mutationFn: async (body) => {
console.log("useCreateServiceToken");
const { data } = await apiRequest.post("/api/v2/service-token/", body); const { data } = await apiRequest.post("/api/v2/service-token/", body);
data.serviceToken += `.${body.randomBytes}`; data.serviceToken += `.${body.randomBytes}`;
return data; return data;
@@ -62,3 +67,55 @@ export const useDeleteServiceToken = () => {
} }
}); });
}; };
export const useCreateServiceTokenV3 = () => {
const queryClient = useQueryClient();
return useMutation<CreateServiceTokenDataV3Res, {}, CreateServiceTokenDataV3DTO>({
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<ServiceTokenDataV3, {}, UpdateServiceTokenDataV3DTO>({
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<ServiceTokenDataV3, {}, DeleteServiceTokenDataV3DTO>({
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));
}
});
};
@@ -33,3 +33,38 @@ export type CreateServiceTokenRes = {
}; };
export type DeleteServiceTokenRes = { serviceTokenData: ServiceToken }; 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;
}
+3 -1
View File
@@ -19,4 +19,6 @@ export {
useReorderWsEnvironment, useReorderWsEnvironment,
useToggleAutoCapitalization, useToggleAutoCapitalization,
useUpdateUserWorkspaceRole, useUpdateUserWorkspaceRole,
useUpdateWsEnvironment} from "./queries"; useUpdateWsEnvironment,
useGetWorkspaceServiceTokenDataV3
} from "./queries";
+19 -1
View File
@@ -6,6 +6,7 @@ import { IntegrationAuth } from "../integrationAuth/types";
import { TIntegration } from "../integrations/types"; import { TIntegration } from "../integrations/types";
import { EncryptedSecret } from "../secrets/types"; import { EncryptedSecret } from "../secrets/types";
import { TWorkspaceUser } from "../users/types"; import { TWorkspaceUser } from "../users/types";
import { ServiceTokenDataV3 } from "../serviceTokens/types";
import { import {
CreateEnvironmentDTO, CreateEnvironmentDTO,
CreateWorkspaceDTO, CreateWorkspaceDTO,
@@ -32,7 +33,8 @@ export const workspaceKeys = {
getAllUserWorkspace: ["workspaces"] as const, getAllUserWorkspace: ["workspaces"] as const,
getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const, getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const,
getWorkspaceAuditLogs: (workspaceId: string) => [{ 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) => { 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
});
};
@@ -49,56 +49,54 @@ export const APIKeyTable = () => {
}; };
return ( return (
<div> <TableContainer>
<TableContainer className=""> <Table>
<Table> <THead>
<THead> <Tr>
<Th className="flex-1">Name</Th>
<Th className="flex-1">Last active</Th>
<Th className="flex-1">Created</Th>
<Th className="flex-1">Expiration</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="api-keys" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({ _id, name, createdAt, expiresAt, lastUsed }) => {
return (
<Tr className="h-10" key={`api-key-${_id}`}>
<Td>{name}</Td>
<Td>{formatDate(lastUsed)}</Td>
<Td>{formatDate(createdAt)}</Td>
<Td>{formatDate(expiresAt)}</Td>
<Td>
<IconButton
onClick={async () => {
await handleDeleteAPIKeyDataClick(_id);
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr> <Tr>
<Th className="flex-1">Name</Th> <Td colSpan={5}>
<Th className="flex-1">Last active</Th> <EmptyState title="No API Keys on file" icon={faKey} />
<Th className="flex-1">Created</Th> </Td>
<Th className="flex-1">Expiration</Th>
<Th className="w-5" />
</Tr> </Tr>
</THead> )}
<TBody> </TBody>
{isLoading && <TableSkeleton columns={4} innerKey="api-keys" />} </Table>
{!isLoading && </TableContainer>
data &&
data.length > 0 &&
data.map(({ _id, name, createdAt, expiresAt, lastUsed }) => {
return (
<Tr className="h-10" key={`api-key-${_id}`}>
<Td>{name}</Td>
<Td>{formatDate(lastUsed)}</Td>
<Td>{formatDate(createdAt)}</Td>
<Td>{formatDate(expiresAt)}</Td>
<Td>
<IconButton
onClick={async () => {
await handleDeleteAPIKeyDataClick(_id);
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No API Keys on file" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</div>
); );
}; };
@@ -161,18 +161,18 @@ export const AddAPIKeyModal = ({
)} )}
/> />
<div className="mt-8 flex items-center"> <div className="mt-8 flex items-center">
<Button <Button
className="mr-4" className="mr-4"
size="sm" size="sm"
type="submit" type="submit"
isLoading={isLoading} isLoading={isLoading}
isDisabled={isLoading} isDisabled={isLoading}
> >
Add Add
</Button> </Button>
<Button colorSchema="secondary" variant="plain"> <Button colorSchema="secondary" variant="plain">
Cancel Cancel
</Button> </Button>
</div> </div>
</form> </form>
) : ( ) : (
@@ -2,8 +2,6 @@ import { Fragment } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import NavHeader from "@app/components/navigation/NavHeader";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab"; import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
import { ProjectServiceTokensTab } from "./components/ProjectServiceTokensTab"; import { ProjectServiceTokensTab } from "./components/ProjectServiceTokensTab";
import { WebhooksTab } from "./components/WebhooksTab"; import { WebhooksTab } from "./components/WebhooksTab";
@@ -17,12 +15,9 @@ const tabs = [
export const ProjectSettingsPage = () => { export const ProjectSettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="flex w-full justify-center bg-bunker-800 px-6 text-white"> <div className="flex justify-center w-full h-full bg-bunker-800 text-white">
<div className="w-full max-w-screen-lg"> <div className="max-w-7xl px-6 w-full">
<div className="relative right-5 ml-4"> <div className="my-6">
<NavHeader pageName={t("settings.project.title")} isProjectRelated />
</div>
<div className="my-8">
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p> <p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
</div> </div>
<Tab.Group> <Tab.Group>
@@ -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<typeof schema>;
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<FormData>({
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 ( return (
<div> <Modal
AddServiceTokenV3Modal isOpen={popUp?.createServiceTokenV3?.isOpen}
</div> onOpenChange={(isOpen) => {
handlePopUpToggle("createServiceTokenV3", isOpen);
reset();
}}
>
<ModalContent title="Create Service Token V3">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="My ST V3"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="expiresIn"
defaultValue="15552000"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Expiration"
errorText={error?.message}
isError={Boolean(error)}
>
<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}
>
Create
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
); );
} }
@@ -1,14 +1,34 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AddServiceTokenV3Modal } from "./AddServiceTokenV3Modal"; import { AddServiceTokenV3Modal } from "./AddServiceTokenV3Modal";
import { ServiceTokenV3Table } from "./ServiceTokenV3Table"; import { ServiceTokenV3Table } from "./ServiceTokenV3Table";
import { Button } from "@app/components/v2";
import { usePopUp } from "@app/hooks/usePopUp";
export const ServiceTokenV3Section = () => { export const ServiceTokenV3Section = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"createServiceTokenV3"
] as const);
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="text-xl font-semibold text-mineshaft-100"> <div className="flex justify-between mb-8">
Service Tokens 2.0 <p className="text-xl font-semibold text-mineshaft-100">
</p> Service Tokens 2.0
</p>
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("createServiceTokenV3")}
>
Create ST V3
</Button>
</div>
<ServiceTokenV3Table /> <ServiceTokenV3Table />
<AddServiceTokenV3Modal /> <AddServiceTokenV3Modal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
</div> </div>
); );
} }
@@ -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 = () => { 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 ( return (
<div> <TableContainer>
ServiceTokenV3Table <Table>
</div> <THead>
<Tr>
<Th>Name</Th>
<Th>Status</Th>
<Th>Last Active</Th>
<Th>Created</Th>
<Th>Expiration</Th>
<Th className="w-5"></Th>
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={5} innerKey="service-tokens" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({
_id,
name,
isActive,
lastUsed,
createdAt,
// 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>
</Td>
<Td>{lastUsed ? formatDate(lastUsed) : "-"}</Td>
<Td>{formatDate(createdAt)}</Td>
<Td>{formatDate(createdAt)}</Td>
<Td className="flex justify-end">
<IconButton
onClick={async () => {
console.log("edit");
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
onClick={() => handleDeleteServiceTokenData(_id)}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No service token v3 on file" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
); );
} }