Merge branch 'main' of github.com:atimapreandrew/infisical

This commit is contained in:
Andrew Atimapre
2023-06-28 19:15:08 +01:00
76 changed files with 7735 additions and 4377 deletions
@@ -53,7 +53,7 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
req.authData.authPayload._id req.authData.authPayload._id
) )
.select("+encryptedKey +iv +tag") .select("+encryptedKey +iv +tag")
.populate("user"); .populate("user").lean();
return res.status(200).json(serviceTokenData); return res.status(200).json(serviceTokenData);
}; };
@@ -3,8 +3,18 @@ import { getLicenseServerUrl } from "../../../config";
import { licenseServerKeyRequest } from "../../../config/request"; import { licenseServerKeyRequest } from "../../../config/request";
import { EELicenseService } from "../../services"; import { EELicenseService } from "../../services";
export const getOrganizationPlansTable = async (req: Request, res: Response) => {
const billingCycle = req.query.billingCycle as string;
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
);
return res.status(200).send(data);
}
/** /**
* Return the organization's current plan and allowed feature set * Return the organization current plan's feature set
*/ */
export const getOrganizationPlan = async (req: Request, res: Response) => { export const getOrganizationPlan = async (req: Request, res: Response) => {
const { organizationId } = req.params; const { organizationId } = req.params;
@@ -18,26 +28,58 @@ export const getOrganizationPlan = async (req: Request, res: Response) => {
} }
/** /**
* Update the organization plan to product with id [productId] * Return the organization's current plan's billing info
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateOrganizationPlan = async (req: Request, res: Response) => { export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => {
const { const { data } = await licenseServerKeyRequest.get(
productId, `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/billing`
} = req.body;
const { data } = await licenseServerKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan`,
{
productId,
}
); );
return res.status(200).send(data); return res.status(200).send(data);
} }
/**
* Return the organization's current plan's feature table
* @param req
* @param res
* @returns
*/
export const getOrganizationPlanTable = async (req: Request, res: Response) => {
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/table`
);
return res.status(200).send(data);
}
export const getOrganizationBillingDetails = async (req: Request, res: Response) => {
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details`
);
return res.status(200).send(data);
}
export const updateOrganizationBillingDetails = async (req: Request, res: Response) => {
const {
name,
email
} = req.body;
const { data } = await licenseServerKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details`,
{
...(name ? { name } : {}),
...(email ? { email } : {})
}
);
return res.status(200).send(data);
}
/** /**
* Return the organization's payment methods on file * Return the organization's payment methods on file
*/ */
@@ -46,9 +88,7 @@ export const getOrganizationPmtMethods = async (req: Request, res: Response) =>
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods` `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`
); );
return res.status(200).send({ return res.status(200).send(pmtMethods);
pmtMethods,
});
} }
/** /**
@@ -81,4 +121,53 @@ export const deleteOrganizationPmtMethod = async (req: Request, res: Response) =
); );
return res.status(200).send(data); return res.status(200).send(data);
}
/**
* Return the organization's tax ids on file
*/
export const getOrganizationTaxIds = async (req: Request, res: Response) => {
const { data: { tax_ids } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids`
);
return res.status(200).send(tax_ids);
}
/**
* Add tax id to organization
*/
export const addOrganizationTaxId = async (req: Request, res: Response) => {
const {
type,
value
} = req.body;
const { data } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids`,
{
type,
value
}
);
return res.status(200).send(data);
}
export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
const { taxId } = req.params;
const { data } = await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids/${taxId}`,
);
return res.status(200).send(data);
}
export const getOrganizationInvoices = async (req: Request, res: Response) => {
const { data: { invoices } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices`
);
return res.status(200).send(invoices);
} }
+130 -10
View File
@@ -11,10 +11,25 @@ import {
ACCEPTED, ADMIN, MEMBER, OWNER, ACCEPTED, ADMIN, MEMBER, OWNER,
} from "../../../variables"; } from "../../../variables";
router.get(
"/:organizationId/plans/table",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("billingCycle").exists().isString().isIn(["monthly", "yearly"]),
validateRequest,
organizationsController.getOrganizationPlansTable
);
router.get( router.get(
"/:organizationId/plan", "/:organizationId/plan",
requireAuth({ requireAuth({
acceptedAuthModes: ["jwt", "apiKey"], acceptedAuthModes: ["jwt"],
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
@@ -26,25 +41,70 @@ router.get(
organizationsController.getOrganizationPlan organizationsController.getOrganizationPlan
); );
router.patch( router.get(
"/:organizationId/plan", "/:organizationId/plan/billing",
requireAuth({ requireAuth({
acceptedAuthModes: ["jwt", "apiKey"], acceptedAuthModes: ["jwt"],
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED], acceptedStatuses: [ACCEPTED],
}), }),
param("organizationId").exists().trim(), param("organizationId").exists().trim(),
body("productId").exists().isString(), query("workspaceId").optional().isString(),
validateRequest, validateRequest,
organizationsController.updateOrganizationPlan organizationsController.getOrganizationPlanBillingInfo
);
router.get(
"/:organizationId/plan/table",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("workspaceId").optional().isString(),
validateRequest,
organizationsController.getOrganizationPlanTable
);
router.get(
"/:organizationId/billing-details",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationBillingDetails
);
router.patch(
"/:organizationId/billing-details",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("email").optional().isString().trim(),
body("name").optional().isString().trim(),
validateRequest,
organizationsController.updateOrganizationBillingDetails
); );
router.get( router.get(
"/:organizationId/billing-details/payment-methods", "/:organizationId/billing-details/payment-methods",
requireAuth({ requireAuth({
acceptedAuthModes: ["jwt", "apiKey"], acceptedAuthModes: ["jwt"],
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
@@ -58,7 +118,7 @@ router.get(
router.post( router.post(
"/:organizationId/billing-details/payment-methods", "/:organizationId/billing-details/payment-methods",
requireAuth({ requireAuth({
acceptedAuthModes: ["jwt", "apiKey"], acceptedAuthModes: ["jwt"],
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
@@ -74,7 +134,22 @@ router.post(
router.delete( router.delete(
"/:organizationId/billing-details/payment-methods/:pmtMethodId", "/:organizationId/billing-details/payment-methods/:pmtMethodId",
requireAuth({ requireAuth({
acceptedAuthModes: ["jwt", "apiKey"], acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
param("pmtMethodId").exists().trim(),
validateRequest,
organizationsController.deleteOrganizationPmtMethod
);
router.get(
"/:organizationId/billing-details/tax-ids",
requireAuth({
acceptedAuthModes: ["jwt"],
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
@@ -82,7 +157,52 @@ router.delete(
}), }),
param("organizationId").exists().trim(), param("organizationId").exists().trim(),
validateRequest, validateRequest,
organizationsController.deleteOrganizationPmtMethod organizationsController.getOrganizationTaxIds
);
router.post(
"/:organizationId/billing-details/tax-ids",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("type").exists().isString(),
body("value").exists().isString(),
validateRequest,
organizationsController.addOrganizationTaxId
);
router.delete(
"/:organizationId/billing-details/tax-ids/:taxId",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
param("taxId").exists().trim(),
validateRequest,
organizationsController.deleteOrganizationTaxId
);
router.get(
"/:organizationId/invoices",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationInvoices
); );
export default router; export default router;
+1 -1
View File
@@ -55,7 +55,7 @@ class EELicenseService {
environmentLimit: null, environmentLimit: null,
environmentsUsed: 0, environmentsUsed: 0,
secretVersioning: true, secretVersioning: true,
pitRecovery: true, pitRecovery: false,
rbac: true, rbac: true,
customRateLimits: true, customRateLimits: true,
customAlerts: true, customAlerts: true,
+1 -1
View File
@@ -188,7 +188,7 @@ type GetServiceTokenDetailsResponse struct {
Tag string `json:"tag"` Tag string `json:"tag"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
V int `json:"__v"` SecretPath string `json:"secretPath"`
} }
type GetAccessibleEnvironmentsRequest struct { type GetAccessibleEnvironmentsRequest struct {
+1
View File
@@ -37,6 +37,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{ encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace, WorkspaceId: serviceTokenDetails.Workspace,
Environment: serviceTokenDetails.Environment, Environment: serviceTokenDetails.Environment,
SecretPath: serviceTokenDetails.SecretPath,
}) })
if err != nil { if err != nil {
+8 -1
View File
@@ -6,7 +6,14 @@ module.exports = {
'@storybook/addon-essentials', '@storybook/addon-essentials',
'@storybook/addon-interactions', '@storybook/addon-interactions',
'storybook-dark-mode', 'storybook-dark-mode',
'@storybook/addon-postcss' {
name: '@storybook/addon-styling',
options: {
postCss: {
implementation: require('postcss')
}
}
}
], ],
framework: { framework: {
name: '@storybook/nextjs', name: '@storybook/nextjs',
+4493 -3451
View File
File diff suppressed because it is too large Load Diff
+13 -13
View File
@@ -7,7 +7,7 @@
"start": "next start", "start": "next start",
"start:docker": "next build && next start", "start:docker": "next build && next start",
"lint": "eslint --ext js,ts,tsx ./src", "lint": "eslint --ext js,ts,tsx ./src",
"lint-and-fix": "eslint --fix --ext js,ts,tsx ./src", "lint:fix": "eslint --fix --ext js,ts,tsx ./src",
"type-check": "tsc --project tsconfig.json", "type-check": "tsc --project tsconfig.json",
"storybook": "storybook dev -p 6006 -s ./public", "storybook": "storybook dev -p 6006 -s ./public",
"build-storybook": "storybook build" "build-storybook": "storybook build"
@@ -62,8 +62,8 @@
"infisical-node": "^1.0.37", "infisical-node": "^1.0.37",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"lottie-react": "^2.4.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lottie-react": "^2.4.0",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"next": "^12.3.4", "next": "^12.3.4",
"posthog-js": "^1.58.0", "posthog-js": "^1.58.0",
@@ -91,14 +91,14 @@
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^7.0.0-beta.30", "@storybook/addon-essentials": "^7.0.23",
"@storybook/addon-interactions": "^7.0.0-beta.30", "@storybook/addon-interactions": "^7.0.23",
"@storybook/addon-links": "^7.0.0-beta.30", "@storybook/addon-links": "^7.0.23",
"@storybook/addon-postcss": "^2.0.0", "@storybook/addon-styling": "^1.3.0",
"@storybook/blocks": "^7.0.0-beta.30", "@storybook/blocks": "^7.0.23",
"@storybook/nextjs": "^7.0.0-beta.30", "@storybook/nextjs": "^7.0.23",
"@storybook/react": "^7.0.0-beta.30", "@storybook/react": "^7.0.23",
"@storybook/testing-library": "^0.0.13", "@storybook/testing-library": "^0.2.0",
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/typography": "^0.5.4",
"@types/jsrp": "^0.2.4", "@types/jsrp": "^0.2.4",
"@types/node": "18.11.9", "@types/node": "18.11.9",
@@ -118,12 +118,12 @@
"eslint-plugin-react": "^7.32.0", "eslint-plugin-react": "^7.32.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^8.0.0", "eslint-plugin-simple-import-sort": "^8.0.0",
"eslint-plugin-storybook": "^0.6.10", "eslint-plugin-storybook": "^0.6.12",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"prettier": "^2.8.3", "prettier": "^2.8.3",
"prettier-plugin-tailwindcss": "^0.2.2", "prettier-plugin-tailwindcss": "^0.2.2",
"storybook": "^7.0.0-beta.30", "storybook": "^7.0.23",
"storybook-dark-mode": "^2.0.5", "storybook-dark-mode": "^3.0.0",
"tailwindcss": "3.2", "tailwindcss": "3.2",
"typescript": "^4.9.3" "typescript": "^4.9.3"
} }
@@ -97,7 +97,7 @@ export default function InitialLoginStep({
setIsLoading(false); setIsLoading(false);
} }
return <div className='flex flex-col mx-auto w-full justify-center items-center'> return <div className='flex flex-col mx-auto w-full justify-center items-center'>
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >Login to Infisical</h1> <h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >Login to Infisical</h1>
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'> {/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
@@ -57,7 +57,7 @@ export default function NavHeader({
); );
return ( return (
<div className="ml-6 flex flex-row items-center pt-6"> <div className="ml-4 flex flex-row items-center pt-6">
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100"> <div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
{currentOrg?.name?.charAt(0)} {currentOrg?.name?.charAt(0)}
</div> </div>
@@ -13,6 +13,7 @@ type Props = {
onChange?: (isOpen: boolean) => void; onChange?: (isOpen: boolean) => void;
deleteKey: string; deleteKey: string;
title: string; title: string;
subTitle?: string;
onDeleteApproved: () => Promise<void>; onDeleteApproved: () => Promise<void>;
}; };
@@ -22,7 +23,8 @@ export const DeleteActionModal = ({
onChange, onChange,
deleteKey, deleteKey,
onDeleteApproved, onDeleteApproved,
title title,
subTitle = "This action is irreversible!"
}: Props): JSX.Element => { }: Props): JSX.Element => {
const [inputData, setInputData] = useState(""); const [inputData, setInputData] = useState("");
const [isLoading, setIsLoading] = useToggle(); const [isLoading, setIsLoading] = useToggle();
@@ -52,7 +54,7 @@ export const DeleteActionModal = ({
> >
<ModalContent <ModalContent
title={title} title={title}
subTitle="This action is irreversible!" subTitle={subTitle}
footerContent={ footerContent={
<div className="flex items-center"> <div className="flex items-center">
<Button <Button
@@ -11,7 +11,7 @@ export type FormLabelProps = {
}; };
export const FormLabel = ({ id, label, isRequired }: FormLabelProps) => ( export const FormLabel = ({ id, label, isRequired }: FormLabelProps) => (
<Label.Root className="mb-1 ml-0.5 block text-sm font-medium text-mineshaft-300" htmlFor={id}> <Label.Root className="mb-0.5 ml-1 block text-sm font-normal text-mineshaft-400" htmlFor={id}>
{label} {label}
{isRequired && <span className="ml-1 text-red">*</span>} {isRequired && <span className="ml-1 text-red">*</span>}
</Label.Root> </Label.Root>
@@ -1,5 +1,7 @@
import Link from "next/link"; import Link from "next/link";
import { useSubscription } from "@app/context";
import { Button } from "../Button"; import { Button } from "../Button";
import { Modal, ModalClose, ModalContent } from "../Modal"; import { Modal, ModalClose, ModalContent } from "../Modal";
@@ -9,28 +11,35 @@ type Props = {
text: string; text: string;
}; };
export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => ( export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => {
<Modal isOpen={isOpen} onOpenChange={onOpenChange}> const { subscription } = useSubscription();
<ModalContent const link = (subscription && subscription.slug !== null)
title="Unleash Infisical's Full Power" ? `/settings/billing/${localStorage.getItem("projectData.id") as string}`
footerContent={[ : "https://infisical.com/scheduledemo";
<Link
href={`/settings/billing/${localStorage.getItem("projectData.id") as string}`} return (
key="upgrade-plan" <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
> <ModalContent
<Button className="mr-4 ml-2 mb-2">Upgrade Plan</Button> title="Unleash Infisical's Full Power"
</Link>, footerContent={[
<ModalClose asChild key="upgrade-plan-cancel"> <Link
<Button colorSchema="secondary" variant="plain"> href={link}
Cancel key="upgrade-plan"
</Button> >
</ModalClose> <Button className="mr-4 ml-2 mb-2">Upgrade Plan</Button>
]} </Link>,
> <ModalClose asChild key="upgrade-plan-cancel">
<p className="mb-2 text-bunker-300">{text}</p> <Button colorSchema="secondary" variant="plain">
<p className="text-bunker-300"> Cancel
Upgrade and get access to this, as well as to other powerful enhancements. </Button>
</p> </ModalClose>
</ModalContent> ]}
</Modal> >
); <p className="mb-2 text-bunker-300">{text}</p>
<p className="text-bunker-300">
Upgrade and get access to this, as well as to other powerful enhancements.
</p>
</ModalContent>
</Modal>
)
}
+6 -5
View File
@@ -1,6 +1,7 @@
export { export {
useGetAuthToken, useGetAuthToken,
useGetCommonPasswords, useGetCommonPasswords,
useRevokeAllSessions, useRevokeAllSessions,
useSendMfaToken, useSendMfaToken,
useVerifyMfaToken} from "./queries" useVerifyMfaToken
} from "./queries";
+1
View File
@@ -0,0 +1 @@
export { useGetWorkspaceBot, useUpdateBotActiveStatus } from "./queries";
+38
View File
@@ -0,0 +1,38 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TBot, TSetBotActiveStatusDto } from "./types";
const queryKeys = {
getBot: (workspaceId: string) => [{ workspaceId }, "bot"] as const
};
const fetchWorkspaceBot = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ bot: TBot }>(`/api/v1/bot/${workspaceId}`);
return data.bot;
};
export const useGetWorkspaceBot = (workspaceId: string) =>
useQuery({
queryKey: queryKeys.getBot(workspaceId),
queryFn: () => fetchWorkspaceBot(workspaceId),
enabled: Boolean(workspaceId)
});
// mutation
export const useUpdateBotActiveStatus = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TSetBotActiveStatusDto>({
mutationFn: ({ botId, isActive, botKey }) =>
apiRequest.patch(`/api/v1/bot/${botId}/active`, {
isActive,
botKey
}),
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(queryKeys.getBot(workspaceId));
}
});
};
+20
View File
@@ -0,0 +1,20 @@
export type TBot = {
_id: string;
name: string;
workspace: string;
isActive: boolean;
publicKey: string;
createdAt: string;
updatedAt: string;
__v: number;
};
export type TSetBotActiveStatusDto = {
workspaceId: string;
botId: string;
isActive: boolean;
botKey?: {
encryptedKey: string;
nonce: string;
};
};
+3
View File
@@ -1,5 +1,8 @@
export * from "./auth"; export * from "./auth";
export * from "./bots";
export * from "./incidentContacts"; export * from "./incidentContacts";
export * from "./integrationAuth";
export * from "./integrations";
export * from "./keys"; export * from "./keys";
export * from "./organization"; export * from "./organization";
export * from "./secretFolders"; export * from "./secretFolders";
@@ -1,7 +1,9 @@
export { export {
useGetIntegrationAuthApps, useDeleteIntegrationAuth,
useGetIntegrationAuthById, useGetIntegrationAuthApps,
useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthById,
useGetIntegrationAuthRailwayServices, useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthTeams, useGetIntegrationAuthRailwayServices,
useGetIntegrationAuthVercelBranches} from "./queries"; useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches
} from "./queries";
+198 -165
View File
@@ -1,204 +1,237 @@
import { useQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { import { workspaceKeys } from "../workspace/queries";
App, import { App, Environment, IntegrationAuth, Service, Team } from "./types";
Environment,
IntegrationAuth,
Service,
Team
} from "./types";
const integrationAuthKeys = { const integrationAuthKeys = {
getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuth"] as const, getIntegrationAuthById: (integrationAuthId: string) =>
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) => [{ integrationAuthId, teamId }, "integrationAuthApps"] as const, [{ integrationAuthId }, "integrationAuth"] as const,
getIntegrationAuthTeams: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuthTeams"] as const, getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) =>
getIntegrationAuthVercelBranches: ({ [{ integrationAuthId, teamId }, "integrationAuthApps"] as const,
integrationAuthId, getIntegrationAuthTeams: (integrationAuthId: string) =>
appId, [{ integrationAuthId }, "integrationAuthTeams"] as const,
}: { getIntegrationAuthVercelBranches: ({
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
getIntegrationAuthRailwayEnvironments: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayEnvironments"] as const,
getIntegrationAuthRailwayServices: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const
}
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ integrationAuth: IntegrationAuth }>(`/api/v1/integration-auth/${integrationAuthId}`);
return data.integrationAuth;
}
const fetchIntegrationAuthApps = async ({
integrationAuthId,
teamId
}: {
integrationAuthId: string;
teamId?: string;
}) => {
const searchParams = new URLSearchParams(teamId ? { teamId } : undefined);
const { data } = await apiRequest.get<{ apps: App[] }>(
`/api/v1/integration-auth/${integrationAuthId}/apps`,
{ params: searchParams }
);
return data.apps;
}
const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ teams: Team[] }>(`/api/v1/integration-auth/${integrationAuthId}/teams`);
return data.teams;
}
const fetchIntegrationAuthVercelBranches = async ({
integrationAuthId, integrationAuthId,
appId appId
}: { }: {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
getIntegrationAuthRailwayEnvironments: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayEnvironments"] as const,
getIntegrationAuthRailwayServices: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const
};
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ integrationAuth: IntegrationAuth }>(
`/api/v1/integration-auth/${integrationAuthId}`
);
return data.integrationAuth;
};
const fetchIntegrationAuthApps = async ({
integrationAuthId,
teamId
}: {
integrationAuthId: string;
teamId?: string;
}) => { }) => {
const { data: { branches } } = await apiRequest.get<{ branches: string[] }>(`/api/v1/integration-auth/${integrationAuthId}/vercel/branches`, { const searchParams = new URLSearchParams(teamId ? { teamId } : undefined);
params: { const { data } = await apiRequest.get<{ apps: App[] }>(
appId `/api/v1/integration-auth/${integrationAuthId}/apps`,
} { params: searchParams }
}); );
return data.apps;
return branches; };
const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ teams: Team[] }>(
`/api/v1/integration-auth/${integrationAuthId}/teams`
);
return data.teams;
};
const fetchIntegrationAuthVercelBranches = async ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
const {
data: { branches }
} = await apiRequest.get<{ branches: string[] }>(
`/api/v1/integration-auth/${integrationAuthId}/vercel/branches`,
{
params: {
appId
}
}
);
return branches;
}; };
const fetchIntegrationAuthRailwayEnvironments = async ({ const fetchIntegrationAuthRailwayEnvironments = async ({
integrationAuthId, integrationAuthId,
appId appId
}: { }: {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => { }) => {
const { data: { environments } } = await apiRequest.get<{ environments: Environment[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/environments`, { const {
params: { data: { environments }
appId } = await apiRequest.get<{ environments: Environment[] }>(
} `/api/v1/integration-auth/${integrationAuthId}/railway/environments`,
}); {
params: {
return environments; appId
} }
}
);
return environments;
};
const fetchIntegrationAuthRailwayServices = async ({ const fetchIntegrationAuthRailwayServices = async ({
integrationAuthId, integrationAuthId,
appId appId
}: { }: {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => { }) => {
const { data: { services } } = await apiRequest.get<{ services: Service[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/services`, { const {
params: { data: { services }
appId } = await apiRequest.get<{ services: Service[] }>(
} `/api/v1/integration-auth/${integrationAuthId}/railway/services`,
}); {
params: {
return services; appId
} }
}
);
return services;
};
export const useGetIntegrationAuthById = (integrationAuthId: string) => { export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId), queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
queryFn: () => fetchIntegrationAuthById(integrationAuthId), queryFn: () => fetchIntegrationAuthById(integrationAuthId),
enabled: true enabled: true
}); });
} };
export const useGetIntegrationAuthApps = ({ export const useGetIntegrationAuthApps = ({
integrationAuthId, integrationAuthId,
teamId teamId
}: { }: {
integrationAuthId: string; integrationAuthId: string;
teamId?: string; teamId?: string;
}) => { }) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId), queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
queryFn: () => fetchIntegrationAuthApps({ queryFn: () =>
integrationAuthId, fetchIntegrationAuthApps({
teamId integrationAuthId,
}), teamId
enabled: true }),
}); enabled: true
} });
};
export const useGetIntegrationAuthTeams = (integrationAuthId: string) => { export const useGetIntegrationAuthTeams = (integrationAuthId: string) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthTeams(integrationAuthId), queryKey: integrationAuthKeys.getIntegrationAuthTeams(integrationAuthId),
queryFn: () => fetchIntegrationAuthTeams(integrationAuthId), queryFn: () => fetchIntegrationAuthTeams(integrationAuthId),
enabled: true enabled: true
}); });
} };
export const useGetIntegrationAuthVercelBranches = ({ export const useGetIntegrationAuthVercelBranches = ({
integrationAuthId, integrationAuthId,
appId, appId
}: { }: {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => { }) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({ queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({
integrationAuthId, integrationAuthId,
appId, appId
}), }),
queryFn: () => fetchIntegrationAuthVercelBranches({ queryFn: () =>
integrationAuthId, fetchIntegrationAuthVercelBranches({
appId, integrationAuthId,
}), appId
enabled: true }),
}); enabled: true
} });
};
export const useGetIntegrationAuthRailwayEnvironments = ({ export const useGetIntegrationAuthRailwayEnvironments = ({
integrationAuthId, integrationAuthId,
appId appId
}: { }: {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => { }) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({ queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({
integrationAuthId, integrationAuthId,
appId, appId
}), }),
queryFn: () => fetchIntegrationAuthRailwayEnvironments({ queryFn: () =>
integrationAuthId, fetchIntegrationAuthRailwayEnvironments({
appId, integrationAuthId,
}), appId
enabled: true }),
}); enabled: true
} });
};
export const useGetIntegrationAuthRailwayServices = ({ export const useGetIntegrationAuthRailwayServices = ({
integrationAuthId, integrationAuthId,
appId appId
}: { }: {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => { }) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({ queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({
integrationAuthId, integrationAuthId,
appId, appId
}), }),
queryFn: () => fetchIntegrationAuthRailwayServices({ queryFn: () =>
integrationAuthId, fetchIntegrationAuthRailwayServices({
appId, integrationAuthId,
}), appId
enabled: true }),
}); enabled: true
} });
};
export const useDeleteIntegrationAuth = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, { id: string; workspaceId: string }>({
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration-auth/${id}`),
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId));
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
}
});
};
+22 -19
View File
@@ -1,28 +1,31 @@
export type IntegrationAuth = { export type IntegrationAuth = {
_id: string; _id: string;
workspace: string; integration: string;
integration: string; workspace: string;
teamId?: string; __v: number;
accountId?: string; createdAt: string;
} updatedAt: string;
algorithm: string;
keyEncoding: string;
};
export type App = { export type App = {
name: string; name: string;
appId?: string; appId?: string;
owner?: string; owner?: string;
} };
export type Team = { export type Team = {
name: string; name: string;
teamId: string; teamId: string;
} };
export type Environment = { export type Environment = {
name: string; name: string;
environmentId: string; environmentId: string;
} };
export type Service = { export type Service = {
name: string; name: string;
serviceId: string; serviceId: string;
} };
@@ -0,0 +1 @@
export { useDeleteIntegration,useGetCloudIntegrations } from "./queries";
@@ -0,0 +1,35 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries";
import { TCloudIntegration } from "./types";
export const integrationQueryKeys = {
getIntegrations: () => ["integrations"] as const
};
const fetchIntegrations = async () => {
const { data } = await apiRequest.get<{ integrationOptions: TCloudIntegration[] }>(
"/api/v1/integration-auth/integration-options"
);
return data.integrationOptions;
};
export const useGetCloudIntegrations = () =>
useQuery({
queryKey: integrationQueryKeys.getIntegrations(),
queryFn: () => fetchIntegrations()
});
export const useDeleteIntegration = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, { id: string; workspaceId: string }>({
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration/${id}`),
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
}
});
};
@@ -0,0 +1,33 @@
export type TCloudIntegration = {
name: string;
slug: string;
image: string;
isAvailable: boolean;
type: string;
clientId: string;
docsLink: string;
clientSlug: string;
};
export type TIntegration = {
_id: string;
workspace: string;
environment: string;
isActive: boolean;
url: any;
app: string;
appId: string;
targetEnvironment: string;
targetEnvironmentId: string;
targetService: string;
targetServiceId: string;
owner: string;
path: string;
region: string;
integration: string;
integrationAuth: string;
secretPath: string;
createdAt: string;
updatedAt: string;
__v: number;
};
+16 -1
View File
@@ -1 +1,16 @@
export { useGetOrganization, useRenameOrg } from "./queries"; export {
useAddOrgPmtMethod,
useAddOrgTaxId,
useCreateCustomerPortalSession,
useDeleteOrgPmtMethod,
useDeleteOrgTaxId,
useGetOrganization,
useGetOrgBillingDetails,
useGetOrgInvoices,
useGetOrgPlanBillingInfo,
useGetOrgPlansTable,
useGetOrgPlanTable,
useGetOrgPmtMethods,
useGetOrgTaxIds,
useRenameOrg,
useUpdateOrgBillingDetails} from "./queries";
+274 -10
View File
@@ -2,22 +2,39 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { Organization, RenameOrgDTO } from "./types"; import {
BillingDetails,
Invoice,
Organization,
OrgPlanTable,
PlanBillingInfo,
PmtMethod,
ProductsTable,
RenameOrgDTO,
TaxID} from "./types";
const organizationKeys = { const organizationKeys = {
getUserOrganization: ["organization"] as const getUserOrganization: ["organization"] as const,
getOrgPlanBillingInfo: (orgId: string) => [{ orgId }, "organization-plan-billing"] as const,
getOrgPlanTable: (orgId: string) => [{ orgId }, "organization-plan-table"] as const,
getOrgPlansTable: (orgId: string, billingCycle: "monthly" | "yearly") => [{ orgId, billingCycle }, "organization-plans-table"] as const,
getOrgBillingDetails: (orgId: string) => [{ orgId }, "organization-billing-details"] as const,
getOrgPmtMethods: (orgId: string) => [{ orgId }, "organization-pmt-methods"] as const,
getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const,
getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const
}; };
const fetchUserOrganization = async () => { export const useGetOrganization = () => {
const { data } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization"); return useQuery({
queryKey: organizationKeys.getUserOrganization,
queryFn: async () => {
const { data } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization");
return data.organizations; return data.organizations;
}; }
});
}
export const useGetOrganization = () =>
useQuery({ queryKey: organizationKeys.getUserOrganization, queryFn: fetchUserOrganization });
// mutation
export const useRenameOrg = () => { export const useRenameOrg = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -29,3 +46,250 @@ export const useRenameOrg = () => {
} }
}); });
}; };
export const useGetOrgPlanBillingInfo = (organizationId: string) => {
return useQuery({
queryKey: organizationKeys.getOrgPlanBillingInfo(organizationId),
queryFn: async () => {
const { data } = await apiRequest.get<PlanBillingInfo>(
`/api/v1/organizations/${organizationId}/plan/billing`
);
return data;
},
enabled: true
});
}
export const useGetOrgPlanTable = (organizationId: string) => {
return useQuery({
queryKey: organizationKeys.getOrgPlanTable(organizationId),
queryFn: async () => {
const { data } = await apiRequest.get<OrgPlanTable>(
`/api/v1/organizations/${organizationId}/plan/table`
);
return data;
},
enabled: true
});
}
export const useGetOrgPlansTable = ({
organizationId,
billingCycle
}: {
organizationId: string;
billingCycle: "monthly" | "yearly"
}) => {
return useQuery({
queryKey: organizationKeys.getOrgPlansTable(organizationId, billingCycle),
queryFn: async () => {
const { data } = await apiRequest.get<ProductsTable>(
`/api/v1/organizations/${organizationId}/plans/table?billingCycle=${billingCycle}`
);
return data;
},
enabled: true
});
}
export const useGetOrgBillingDetails = (organizationId: string) => {
return useQuery({
queryKey: organizationKeys.getOrgBillingDetails(organizationId),
queryFn: async () => {
const { data } = await apiRequest.get<BillingDetails>(
`/api/v1/organizations/${organizationId}/billing-details`
);
return data;
},
enabled: true
});
}
export const useUpdateOrgBillingDetails = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
organizationId,
name,
email
}: {
organizationId: string;
name?: string;
email?: string;
}) => {
const { data } = await apiRequest.patch(
`/api/v1/organizations/${organizationId}/billing-details`,
{
name,
email
}
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(organizationKeys.getOrgBillingDetails(dto.organizationId));
}
});
};
export const useGetOrgPmtMethods = (organizationId: string) => {
return useQuery({
queryKey: organizationKeys.getOrgPmtMethods(organizationId),
queryFn: async () => {
const { data } = await apiRequest.get<PmtMethod[]>(
`/api/v1/organizations/${organizationId}/billing-details/payment-methods`
);
return data;
},
enabled: true
});
}
export const useAddOrgPmtMethod = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
organizationId,
success_url,
cancel_url
}: {
organizationId: string;
success_url: string;
cancel_url: string;
}) => {
const { data: { url } } = await apiRequest.post(
`/api/v1/organizations/${organizationId}/billing-details/payment-methods`,
{
success_url,
cancel_url
}
);
return url;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(organizationKeys.getOrgPmtMethods(dto.organizationId));
}
});
};
export const useDeleteOrgPmtMethod = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
organizationId,
pmtMethodId,
}: {
organizationId: string;
pmtMethodId: string;
}) => {
const { data } = await apiRequest.delete(
`/api/v1/organizations/${organizationId}/billing-details/payment-methods/${pmtMethodId}`
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(organizationKeys.getOrgPmtMethods(dto.organizationId));
}
});
}
export const useGetOrgTaxIds = (organizationId: string) => {
return useQuery({
queryKey: organizationKeys.getOrgTaxIds(organizationId),
queryFn: async () => {
const { data } = await apiRequest.get<TaxID[]>(
`/api/v1/organizations/${organizationId}/billing-details/tax-ids`
);
return data;
},
enabled: true
});
}
export const useAddOrgTaxId = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
organizationId,
type,
value
}: {
organizationId: string;
type: string;
value: string;
}) => {
const { data } = await apiRequest.post(
`/api/v1/organizations/${organizationId}/billing-details/tax-ids`,
{
type,
value
}
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(organizationKeys.getOrgTaxIds(dto.organizationId));
}
});
};
export const useDeleteOrgTaxId = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
organizationId,
taxId,
}: {
organizationId: string;
taxId: string;
}) => {
const { data } = await apiRequest.delete(
`/api/v1/organizations/${organizationId}/billing-details/tax-ids/${taxId}`
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(organizationKeys.getOrgTaxIds(dto.organizationId));
}
});
}
export const useGetOrgInvoices = (organizationId: string) => {
return useQuery({
queryKey: organizationKeys.getOrgInvoices(organizationId),
queryFn: async () => {
const { data } = await apiRequest.get<Invoice[]>(
`/api/v1/organizations/${organizationId}/invoices`
);
return data;
},
enabled: true
});
}
export const useCreateCustomerPortalSession = () => {
return useMutation({
mutationFn: async (organizationId: string) => {
const { data } = await apiRequest.post(
`/api/v1/organization/${organizationId}/customer-portal-session`
);
return data;
}
});
};
@@ -9,3 +9,79 @@ export type RenameOrgDTO = {
orgId: string; orgId: string;
newOrgName: string; newOrgName: string;
}; };
export type BillingDetails = {
name: string;
email: string;
}
export type PlanBillingInfo = {
amount: number;
currentPeriodEnd: number;
currentPeriodStart: number;
interval: "month" | "year";
intervalCount: number;
quantity: number;
}
export type Invoice = {
_id: string;
created: number;
invoice_pdf: string;
number: string;
paid: boolean;
total: number;
}
export type PmtMethod = {
_id: string;
brand: string;
exp_month: number;
exp_year: number;
funding: string;
last4: string;
}
export type TaxID = {
_id: string;
country: string;
type: string;
value: string;
}
export type OrgPlanTableHead = {
name: string;
}
export type OrgPlanTableRow = {
name: string;
allowed: number | boolean | null;
used: string;
}
export type OrgPlanTable = {
head: OrgPlanTableHead[];
rows: OrgPlanTableRow[];
}
export type ProductsTableHead = {
name: string;
price: number | null;
priceLine: string;
productId: string;
slug: string;
tier: number;
}
export type ProductsTableRow = {
name: string;
starter: number | boolean | null;
team: number | boolean | null;
pro: number | boolean | null;
enterprise: number | boolean | null;
}
export type ProductsTable = {
head: ProductsTableHead[];
rows: ProductsTableRow[];
}
+2
View File
@@ -1,5 +1,7 @@
export type { GetAuthTokenAPI } from "./auth/types"; export type { GetAuthTokenAPI } from "./auth/types";
export type { IncidentContact } from "./incidentContacts/types"; export type { IncidentContact } from "./incidentContacts/types";
export type { IntegrationAuth } from "./integrationAuth/types";
export type { TCloudIntegration, TIntegration } from "./integrations/types";
export type { UserWsKeyPair } from "./keys/types"; export type { UserWsKeyPair } from "./keys/types";
export type { Organization } from "./organization/types"; export type { Organization } from "./organization/types";
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"; export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
@@ -6,8 +6,10 @@ export {
useGetUserWorkspaceMemberships, useGetUserWorkspaceMemberships,
useGetUserWorkspaces, useGetUserWorkspaces,
useGetUserWsEnvironments, useGetUserWsEnvironments,
useGetWorkspaceAuthorizations,
useGetWorkspaceById, useGetWorkspaceById,
useGetWorkspaceIndexStatus, useGetWorkspaceIndexStatus,
useGetWorkspaceIntegrations,
useGetWorkspaceSecrets, useGetWorkspaceSecrets,
useNameWorkspaceSecrets, useNameWorkspaceSecrets,
useRenameWorkspace, useRenameWorkspace,
+50 -13
View File
@@ -2,9 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { import { IntegrationAuth } from "../integrationAuth/types";
EncryptedSecret import { TIntegration } from "../integrations/types";
} from "../secrets/types"; import { EncryptedSecret } from "../secrets/types";
import { import {
CreateEnvironmentDTO, CreateEnvironmentDTO,
CreateWorkspaceDTO, CreateWorkspaceDTO,
@@ -19,11 +19,14 @@ import {
WorkspaceEnv WorkspaceEnv
} from "./types"; } from "./types";
const workspaceKeys = { export const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, "workspace"] as const, getWorkspaceById: (workspaceId: string) => [{ workspaceId }, "workspace"] as const,
getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const, getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const,
getWorkspaceIndexStatus: (workspaceId: string) => [{ workspaceId}, "workspace-index-status"] as const, getWorkspaceIndexStatus: (workspaceId: string) =>
[{ workspaceId }, "workspace-index-status"] as const,
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"], getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
getAllUserWorkspace: ["workspaces"] as const, getAllUserWorkspace: ["workspaces"] as const,
getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const
}; };
@@ -42,15 +45,17 @@ const fetchWorkspaceIndexStatus = async (workspaceId: string) => {
); );
return data; return data;
} };
const fetchWorkspaceSecrets = async (workspaceId: string) => { const fetchWorkspaceSecrets = async (workspaceId: string) => {
const { data: { secrets } } = await apiRequest.get<{ secrets: EncryptedSecret[] }>( const {
data: { secrets }
} = await apiRequest.get<{ secrets: EncryptedSecret[] }>(
`/api/v3/workspaces/${workspaceId}/secrets` `/api/v3/workspaces/${workspaceId}/secrets`
); );
return secrets; return secrets;
} };
const fetchUserWorkspaces = async () => { const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace"); const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
@@ -63,15 +68,15 @@ export const useGetWorkspaceIndexStatus = (workspaceId: string) => {
queryFn: () => fetchWorkspaceIndexStatus(workspaceId), queryFn: () => fetchWorkspaceIndexStatus(workspaceId),
enabled: true enabled: true
}); });
} };
export const useGetWorkspaceSecrets = (workspaceId: string) => { export const useGetWorkspaceSecrets = (workspaceId: string) => {
return useQuery({ return useQuery({
queryKey: workspaceKeys.getWorkspaceSecrets(workspaceId), queryKey: workspaceKeys.getWorkspaceSecrets(workspaceId),
queryFn: () => fetchWorkspaceSecrets(workspaceId), queryFn: () => fetchWorkspaceSecrets(workspaceId),
enabled: true enabled: true
}) });
} };
export const useGetWorkspaceById = (workspaceId: string) => { export const useGetWorkspaceById = (workspaceId: string) => {
return useQuery({ return useQuery({
@@ -126,7 +131,39 @@ export const useNameWorkspaceSecrets = () => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIndexStatus(variables.workspaceId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceIndexStatus(variables.workspaceId));
} }
}); });
} };
const fetchWorkspaceAuthorization = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ authorizations: IntegrationAuth[] }>(
`/api/v1/workspace/${workspaceId}/authorizations`
);
return data.authorizations;
};
export const useGetWorkspaceAuthorizations = <TData = IntegrationAuth[],>(
workspaceId: string,
select?: (data: IntegrationAuth[]) => TData
) =>
useQuery({
queryKey: workspaceKeys.getWorkspaceAuthorization(workspaceId),
queryFn: () => fetchWorkspaceAuthorization(workspaceId),
enabled: Boolean(workspaceId),
select
});
const fetchWorkspaceIntegrations = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ integrations: TIntegration[] }>(
`/api/v1/workspace/${workspaceId}/integrations`
);
return data.integrations;
};
export const useGetWorkspaceIntegrations = (workspaceId: string) =>
useQuery({
queryKey: workspaceKeys.getWorkspaceIntegrations(workspaceId),
queryFn: () => fetchWorkspaceIntegrations(workspaceId),
enabled: Boolean(workspaceId)
});
// mutation // mutation
export const useCreateWorkspace = () => { export const useCreateWorkspace = () => {
+2 -2
View File
@@ -10,14 +10,14 @@ interface UsePopUpProps {
* checks which type of inputProps were given and converts them into key-names * checks which type of inputProps were given and converts them into key-names
* SIDENOTE: On inputting give it as const and not string with (as const) * SIDENOTE: On inputting give it as const and not string with (as const)
*/ */
type UsePopUpState<T extends Readonly<string[]> | UsePopUpProps[]> = { export type UsePopUpState<T extends Readonly<string[]> | UsePopUpProps[]> = {
[P in T extends UsePopUpProps[] ? T[number]["name"] : T[number]]: { [P in T extends UsePopUpProps[] ? T[number]["name"] : T[number]]: {
isOpen: boolean; isOpen: boolean;
data?: unknown; data?: unknown;
}; };
}; };
interface UsePopUpReturn<T extends Readonly<string[]> | UsePopUpProps[]> { export interface UsePopUpReturn<T extends Readonly<string[]> | UsePopUpProps[]> {
popUp: UsePopUpState<T>; popUp: UsePopUpState<T>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<T>, data?: unknown) => void; handlePopUpOpen: (popUpName: keyof UsePopUpState<T>, data?: unknown) => void;
handlePopUpClose: (popUpName: keyof UsePopUpState<T>) => void; handlePopUpClose: (popUpName: keyof UsePopUpState<T>) => void;
@@ -20,7 +20,7 @@ import { Menu, Transition } from "@headlessui/react";
import {TFunction} from "i18next"; import {TFunction} from "i18next";
import guidGenerator from "@app/components/utilities/randomId"; import guidGenerator from "@app/components/utilities/randomId";
import { useOrganization, useUser } from "@app/context"; import { useOrganization, useSubscription,useUser } from "@app/context";
import { useLogoutUser } from "@app/hooks/api"; import { useLogoutUser } from "@app/hooks/api";
const supportOptions = (t: TFunction) => [ const supportOptions = (t: TFunction) => [
@@ -63,7 +63,8 @@ export interface IUser {
*/ */
export const Navbar = () => { export const Navbar = () => {
const router = useRouter(); const router = useRouter();
const { subscription } = useSubscription();
const { currentOrg, orgs } = useOrganization(); const { currentOrg, orgs } = useOrganization();
const { user } = useUser(); const { user } = useUser();
@@ -220,22 +221,24 @@ export const Navbar = () => {
/> />
</div> </div>
</div> </div>
<button {subscription && subscription.slug !== null && (
// onClick={buttonAction} <button
type="button" // onClick={buttonAction}
className="w-full cursor-pointer" type="button"
> className="w-full cursor-pointer"
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => router.push(`/settings/billing/${router.query.id}`)}
className="relative mt-1 flex cursor-pointer select-none justify-start rounded-md py-2 px-2 text-gray-400 duration-200 hover:bg-white/5 hover:text-gray-200"
> >
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faCoins} /> <div
<div className="text-sm">{t("nav.user.usage-billing")}</div> onKeyDown={() => null}
</div> role="button"
</button> tabIndex={0}
onClick={() => router.push(`/settings/billing/${router.query.id}`)}
className="relative mt-1 flex cursor-pointer select-none justify-start rounded-md py-2 px-2 text-gray-400 duration-200 hover:bg-white/5 hover:text-gray-200"
>
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faCoins} />
<div className="text-sm">{t("nav.user.usage-billing")}</div>
</div>
</button>
)}
<button <button
type="button" type="button"
// onClick={buttonAction} // onClick={buttonAction}
+7 -5
View File
@@ -184,11 +184,13 @@ export default function Activity() {
/> />
</div> </div>
</div> </div>
<UpgradePlanModal {subscription && (
isOpen={popUp.upgradePlan.isOpen} <UpgradePlanModal
onOpenChange={() => handlePopUpClose("upgradePlan")} isOpen={popUp.upgradePlan.isOpen}
text="You can see more logs if you switch to Infisical's Business/Professional Plan." onOpenChange={() => handlePopUpClose("upgradePlan")}
/> text={subscription.slug === null ? "You can see more logs under an Enterprise license" : "You can see more logs if you switch to Infisical's Business/Professional Plan."}
/>
)}
</div> </div>
); );
} }
+27 -444
View File
@@ -1,409 +1,18 @@
import crypto from "crypto";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router";
import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json"; import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json";
import ActivateBotDialog from "@app/components/basic/dialog/ActivateBotDialog"; import { IntegrationsPage } from "@app/views/IntegrationsPage";
import CloudIntegrationSection from "@app/components/integrations/CloudIntegrationSection";
import FrameworkIntegrationSection from "@app/components/integrations/FrameworkIntegrationSection";
import IntegrationSection from "@app/components/integrations/IntegrationSection";
import NavHeader from "@app/components/navigation/NavHeader";
import { type Props = {
decryptAssymmetric, frameworkIntegrations: typeof frameworkIntegrationOptions;
encryptAssymmetric };
} from "../../components/utilities/cryptography/crypto";
import getBot from "../api/bot/getBot";
import setBotActiveStatus from "../api/bot/setBotActiveStatus";
import deleteIntegration from "../api/integrations/DeleteIntegration";
import getIntegrationOptions from "../api/integrations/GetIntegrationOptions";
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
import getAWorkspace from "../api/workspace/getAWorkspace";
import getLatestFileKey from "../api/workspace/getLatestFileKey";
interface IntegrationAuth {
_id: string;
integration: string;
workspace: string;
createdAt: string;
updatedAt: string;
}
interface Integration {
_id: string;
isActive: boolean;
app: string | null;
appId: string | null;
createdAt: string;
updatedAt: string;
environment: string;
integration: string;
targetEnvironment: string;
workspace: string;
secretPath:string;
integrationAuth: string;
}
interface IntegrationOption {
tenantId?: string;
clientId: string;
clientSlug?: string; // vercel-integration specific
docsLink: string;
image: string;
isAvailable: boolean;
name: string;
slug: string;
type: string;
}
export default function Integrations() {
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
const [integrationAuths, setIntegrationAuths] = useState<IntegrationAuth[]>([]);
const [environments, setEnvironments] = useState<
{
name: string;
slug: string;
}[]
>([]);
const [integrations, setIntegrations] = useState<Integration[]>([]);
// TODO: These will have its type when migratiing towards react-query
const [bot, setBot] = useState<any>(null);
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
const [selectedIntegrationOption, setSelectedIntegrationOption] =
useState<IntegrationOption | null>(null);
const router = useRouter();
const workspaceId = router.query.id as string;
const Integration = ({ frameworkIntegrations }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => {
(async () => {
try {
const workspace = await getAWorkspace(workspaceId);
setEnvironments(workspace.environments);
// get cloud integration options
setCloudIntegrationOptions(await getIntegrationOptions());
// get project integration authorizations
setIntegrationAuths(
await getWorkspaceAuthorizations({
workspaceId
})
);
// get project integrations
setIntegrations(
await getWorkspaceIntegrations({
workspaceId
})
);
// get project bot
setBot(await getBot({ workspaceId }));
} catch (err) {
console.error(err);
}
})();
}, []);
/**
* Activate bot for project by performing the following steps:
* 1. Get the (encrypted) project key
* 2. Decrypt project key with user's private key
* 3. Encrypt project key with bot's public key
* 4. Send encrypted project key to backend and set bot status to active
*/
const handleBotActivate = async () => {
let botKey;
try {
if (bot) {
// case: there is a bot
const key = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) {
throw new Error("Private Key missing");
}
const WORKSPACE_KEY = decryptAssymmetric({
ciphertext: key.latestKey.encryptedKey,
nonce: key.latestKey.nonce,
publicKey: key.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: WORKSPACE_KEY,
publicKey: bot.publicKey,
privateKey: PRIVATE_KEY
});
botKey = {
encryptedKey: ciphertext,
nonce
};
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: true,
botKey
})
).bot
);
}
} catch (err) {
console.error(err);
}
};
const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
try {
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
let link = "";
switch (integrationOption.slug) {
case "azure-key-vault":
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
break;
case "aws-parameter-store":
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
break;
case "aws-secret-manager":
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
break;
case "heroku":
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
break;
case "vercel":
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break;
case "netlify":
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break;
case "github":
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case "gitlab":
link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
break;
case "render":
link = `${window.location.origin}/integrations/render/authorize`;
break;
case "flyio":
link = `${window.location.origin}/integrations/flyio/authorize`;
break;
case "circleci":
link = `${window.location.origin}/integrations/circleci/authorize`;
break;
case "travisci":
link = `${window.location.origin}/integrations/travisci/authorize`;
break;
case "supabase":
link = `${window.location.origin}/integrations/supabase/authorize`;
break;
case "checkly":
link = `${window.location.origin}/integrations/checkly/authorize`;
break;
case "railway":
link = `${window.location.origin}/integrations/railway/authorize`;
break;
case "hashicorp-vault":
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
break;
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
break;
default:
break;
}
if (link !== "") {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
const handleAuthorizedIntegrationOptionPress = (integrationAuth: IntegrationAuth) => {
try {
let link = "";
switch (integrationAuth.integration) {
case "azure-key-vault":
link = `${window.location.origin}/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
case "aws-parameter-store":
link = `${window.location.origin}/integrations/aws-parameter-store/create?integrationAuthId=${integrationAuth._id}`;
break;
case "aws-secret-manager":
link = `${window.location.origin}/integrations/aws-secret-manager/create?integrationAuthId=${integrationAuth._id}`;
break;
case "heroku":
link = `${window.location.origin}/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`;
break;
case "vercel":
link = `${window.location.origin}/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`;
break;
case "netlify":
link = `${window.location.origin}/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`;
break;
case "github":
link = `${window.location.origin}/integrations/github/create?integrationAuthId=${integrationAuth._id}`;
break;
case "gitlab":
link = `${window.location.origin}/integrations/gitlab/create?integrationAuthId=${integrationAuth._id}`;
break;
case "render":
link = `${window.location.origin}/integrations/render/create?integrationAuthId=${integrationAuth._id}`;
break;
case "flyio":
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
break;
case "circleci":
link = `${window.location.origin}/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`;
break;
case "travisci":
link = `${window.location.origin}/integrations/travisci/create?integrationAuthId=${integrationAuth._id}`;
break;
case "supabase":
link = `${window.location.origin}/integrations/supabase/create?integrationAuthId=${integrationAuth._id}`;
break;
case "checkly":
link = `${window.location.origin}/integrations/checkly/create?integrationAuthId=${integrationAuth._id}`;
break;
case "railway":
link = `${window.location.origin}/integrations/railway/create?integrationAuthId=${integrationAuth._id}`;
break;
case "hashicorp-vault":
link = `${window.location.origin}/integrations/hashicorp-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}
if (link !== "") {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
/**
* Open dialog to activate bot if bot is not active.
* Otherwise, start integration [integrationOption]
* @param {Object} integrationOption - an integration option
* @param {String} integrationOption.name
* @param {String} integrationOption.type
* @param {String} integrationOption.docsLink
* @returns
*/
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
try {
const integrationAuthX = integrationAuths.find(
(integrationAuth) => integrationAuth.integration === integrationOption.slug
);
if (!bot.isActive) {
await handleBotActivate();
}
if (!integrationAuthX) {
// case: integration has not been authorized
handleUnauthorizedIntegrationOptionPress(integrationOption);
return;
}
handleAuthorizedIntegrationOptionPress(integrationAuthX);
} catch (err) {
console.error(err);
}
};
/**
* Handle deleting integration authorization [integrationAuth] and corresponding integrations from state where applicable
* @param {Object} obj
* @param {IntegrationAuth} obj.integrationAuth - integrationAuth to delete
*/
const handleDeleteIntegrationAuth = async ({
integrationAuth: deletedIntegrationAuth
}: {
integrationAuth: IntegrationAuth;
}) => {
try {
const newIntegrations = integrations.filter(
(integration) => integration.integrationAuth !== deletedIntegrationAuth._id
);
setIntegrationAuths(
integrationAuths.filter(
(integrationAuth) => integrationAuth._id !== deletedIntegrationAuth._id
)
);
setIntegrations(newIntegrations);
// handle updating bot
if (newIntegrations.length < 1) {
// case: no integrations left
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: false
})
).bot
);
}
} catch (err) {
console.error(err);
}
};
/**
* Handle deleting integration [integration]
* @param {Object} obj
* @param {Integration} obj.integration - integration to delete
*/
const handleDeleteIntegration = async ({ integration }: { integration: Integration }) => {
try {
const deletedIntegration = await deleteIntegration({
integrationId: integration._id
});
const newIntegrations = integrations.filter((i) => i._id !== deletedIntegration._id);
setIntegrations(newIntegrations);
// handle updating bot
if (newIntegrations.length < 1) {
// case: no integrations left
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: false
})
).bot
);
}
} catch (err) {
console.error(err);
}
};
return ( return (
<div className="flex max-h-full flex-col justify-between bg-bunker-800 text-white"> <>
<Head> <Head>
<title>{t("common.head-title", { title: t("integrations.title") })}</title> <title>{t("common.head-title", { title: t("integrations.title") })}</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
@@ -411,52 +20,26 @@ export default function Integrations() {
<meta property="og:title" content="Manage your .env files in seconds" /> <meta property="og:title" content="Manage your .env files in seconds" />
<meta name="og:description" content={t("integrations.description") as string} /> <meta name="og:description" content={t("integrations.description") as string} />
</Head> </Head>
<div className="no-scrollbar::-webkit-scrollbar h-screen max-h-[calc(100vh-10px)] w-full overflow-y-scroll pb-6 no-scrollbar"> <IntegrationsPage frameworkIntegrations={frameworkIntegrations} />
<NavHeader pageName={t("integrations.title")} isProjectRelated /> </>
<ActivateBotDialog
isOpen={isActivateBotDialogOpen}
closeModal={() => setIsActivateBotDialogOpen(false)}
selectedIntegrationOption={selectedIntegrationOption}
integrationOptionPress={integrationOptionPress}
/>
<IntegrationSection
integrations={integrations}
setIntegrations={setIntegrations}
bot={bot}
setBot={setBot}
environments={environments}
handleDeleteIntegration={handleDeleteIntegration}
/>
{cloudIntegrationOptions.length > 0 && bot ? (
<CloudIntegrationSection
cloudIntegrationOptions={cloudIntegrationOptions}
setSelectedIntegrationOption={setSelectedIntegrationOption as any}
integrationOptionPress={(integrationOption: IntegrationOption) => {
if (!bot.isActive) {
// case: bot is not active -> open modal to activate bot
setIsActivateBotDialogOpen(true);
return;
}
integrationOptionPress(integrationOption);
}}
integrationAuths={integrationAuths}
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
/>
) : (
<>
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">{t("integrations.cloud-integrations")}</h1>
<p className="text-base text-gray-400">{t("integrations.click-to-start")}</p>
</div>
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map(elem => <div key={elem} className="bg-mineshaft-800 border border-mineshaft-600 animate-pulse h-32 rounded-md"/>)}
</div>
</>
)}
<FrameworkIntegrationSection frameworks={frameworkIntegrationOptions as any} />
</div>
</div>
); );
} };
Integrations.requireAuth = true; export const getStaticProps = () => {
return {
props: {
frameworkIntegrations: frameworkIntegrationOptions
}
};
};
export const getStaticPaths = async () => {
return {
paths: [], // indicates that no page needs be created at build time
fallback: "blocking" // indicates the type of fallback
};
};
Integration.requireAuth = true;
export default Integration;
@@ -98,7 +98,7 @@ export default function ChecklyCreateIntegrationPage() {
> >
Checkly Integration Checkly Integration
</CardTitle> </CardTitle>
<FormControl label="Infisical Project Environment" className="mt-2 px-6"> <FormControl label="Infisical Project Environment" className="mt-4 px-6">
<Select <Select
value={selectedSourceEnvironment} value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)} onValueChange={(val) => setSelectedSourceEnvironment(val)}
@@ -114,7 +114,7 @@ export default function ChecklyCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path"> <FormControl label="Secrets Path" className="mt-4 px-6">
<Input <Input
value={secretPath} value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)} onChange={(evt) => setSecretPath(evt.target.value)}
+4 -83
View File
@@ -1,99 +1,20 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Head from "next/head"; import Head from "next/head";
import Plan from "@app/components/billing/Plan"; import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage";
import NavHeader from "@app/components/navigation/NavHeader";
import { useSubscription } from "@app/context";
export default function SettingsBilling() { export default function SettingsBilling() {
const { subscription } = useSubscription();
const { t } = useTranslation(); const { t } = useTranslation();
const plans = [
{
key: 1,
name: t("billing.starter.name")!,
price: t("billing.free")!,
priceExplanation: t("billing.starter.price-explanation")!,
text: t("billing.starter.text")!,
subtext: t("billing.starter.subtext")!,
buttonTextMain: t("billing.downgrade")!,
buttonTextSecondary: t("billing.learn-more")!,
current: subscription?.slug === "starter"
},
{
key: 2,
name: "Team",
price: "$8",
priceExplanation: t("billing.professional.price-explanation")!,
text: "Unlimited members, up to 10 projects. Additional developer experience features.",
buttonTextMain: t("billing.upgrade")!,
buttonTextSecondary: t("billing.learn-more")!,
current: subscription?.slug === "team" || subscription?.slug === "team-annual"
},
{
key: 3,
name: t("billing.professional.name")!,
price: "$18",
priceExplanation: t("billing.professional.price-explanation")!,
text: t("billing.enterprise.text")!,
subtext: t("billing.professional.subtext")!,
buttonTextMain: t("billing.upgrade")!,
buttonTextSecondary: t("billing.learn-more")!,
current: subscription?.slug === "pro" || subscription?.slug === "pro-annual"
},
{
key: 4,
name: t("billing.enterprise.name")!,
price: t("billing.custom-pricing")!,
text: "Boost the security and efficiency of your engineering teams.",
buttonTextMain: t("billing.schedule-demo")!,
buttonTextSecondary: t("billing.learn-more")!,
current: subscription?.slug === "enterprise"
}
];
return ( return (
<div className="flex flex-col justify-between bg-bunker-800 pb-4 text-white"> <div className="h-full bg-bunker-800">
<Head> <Head>
<title>{t("common.head-title", { title: t("billing.title") })}</title> <title>{t("common.head-title", { title: t("billing.title") })}</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Head> </Head>
<div className="flex flex-row"> <BillingSettingsPage />
<div className="w-full pb-2">
<NavHeader pageName={t("billing.title")} />
<div className="my-8 ml-6 flex max-w-5xl flex-row items-center justify-between text-xl">
<div className="flex flex-col items-start justify-start text-3xl">
<p className="mr-4 font-semibold text-gray-200">{t("billing.title")}</p>
<p className="mr-4 text-base font-normal text-gray-400">{t("billing.description")}</p>
</div>
</div>
<div className="ml-6 flex w-max flex-col text-mineshaft-50">
<p className="text-xl font-semibold">{t("billing.subscription")}</p>
<div className="mt-4 grid grid-cols-2 grid-rows-2 gap-y-6 gap-x-3 overflow-x-auto">
{plans.map((plan) => (
<Plan key={plan.name} plan={plan} />
))}
</div>
{/* <p className="mt-12 text-xl font-bold">{t("billing.current-usage")}</p>
<div className="flex flex-row">
<div className="mr-4 mt-8 flex w-60 flex-col items-center justify-center rounded-md bg-white/5 pt-6 pb-10 text-gray-300">
<p className="text-6xl font-bold">{numUsers}</p>
<p className="text-gray-300">
Organization members
</p>
</div>
<div className="mr-4 mt-8 text-gray-300 w-60 pt-6 pb-10 rounded-md bg-white/5 flex justify-center items-center flex flex-col">
<p className="text-6xl font-bold">1 </p>
<p className="text-gray-300">Organization projects</p>
</div>
</div> */}
</div>
</div>
</div>
</div> </div>
); );
} }
SettingsBilling.requireAuth = true; SettingsBilling.requireAuth = true;
@@ -32,10 +32,11 @@ import {
PopoverTrigger, PopoverTrigger,
TableContainer, TableContainer,
Tag, Tag,
Tooltip Tooltip,
UpgradePlanModal
} from "@app/components/v2"; } from "@app/components/v2";
import { leaveConfirmDefaultMessage } from "@app/const"; import { leaveConfirmDefaultMessage } from "@app/const";
import { useWorkspace } from "@app/context"; import { useSubscription,useWorkspace } from "@app/context";
import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks"; import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks";
import { import {
useBatchSecretsOp, useBatchSecretsOp,
@@ -97,6 +98,7 @@ const USER_ACTION_PUSH = "first_time_secrets_pushed";
* They will get it back * They will get it back
*/ */
export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { subscription } = useSubscription();
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
@@ -110,7 +112,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
"uploadedSecOpts", "uploadedSecOpts",
"compareSecrets", "compareSecrets",
"folderForm", "folderForm",
"deleteFolder" "deleteFolder",
"upgradePlan"
] as const); ] as const);
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true); const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
const [searchFilter, setSearchFilter] = useState(""); const [searchFilter, setSearchFilter] = useState("");
@@ -624,7 +627,14 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
<div className="hidden xl:block"> <div className="hidden xl:block">
<Button <Button
variant="outline_bg" variant="outline_bg"
onClick={() => handlePopUpOpen("secretSnapshots")} onClick={() => {
if (subscription && subscription.pitRecovery) {
handlePopUpOpen("secretSnapshots");
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />} leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
isLoading={isLoadingSnapshotCount} isLoading={isLoadingSnapshotCount}
isDisabled={!canDoRollback} isDisabled={!canDoRollback}
@@ -886,6 +896,13 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
</ModalContent> </ModalContent>
</Modal> </Modal>
</FormProvider> </FormProvider>
{subscription && (
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={subscription.slug === null ? "You can perform point-in-time recovery under an Enterprise license" : "You can perform point-in-time recovery if you switch to Infisical's Team plan"}
/>
)}
</div> </div>
); );
}; };
@@ -0,0 +1,105 @@
import crypto from "crypto";
import { TCloudIntegration,UserWsKeyPair } from "@app/hooks/api/types";
import {
decryptAssymmetric,
encryptAssymmetric
} from "../../components/utilities/cryptography/crypto";
export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) {
throw new Error("Private Key missing");
}
const WORKSPACE_KEY = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: WORKSPACE_KEY,
publicKey: botPublicKey,
privateKey: PRIVATE_KEY
});
return { encryptedKey: ciphertext, nonce };
};
export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => {
try {
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
let link = "";
switch (integrationOption.slug) {
case "azure-key-vault":
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
break;
case "aws-parameter-store":
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
break;
case "aws-secret-manager":
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
break;
case "heroku":
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
break;
case "vercel":
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break;
case "netlify":
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break;
case "github":
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case "gitlab":
link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
break;
case "render":
link = `${window.location.origin}/integrations/render/authorize`;
break;
case "flyio":
link = `${window.location.origin}/integrations/flyio/authorize`;
break;
case "circleci":
link = `${window.location.origin}/integrations/circleci/authorize`;
break;
case "travisci":
link = `${window.location.origin}/integrations/travisci/authorize`;
break;
case "supabase":
link = `${window.location.origin}/integrations/supabase/authorize`;
break;
case "checkly":
link = `${window.location.origin}/integrations/checkly/authorize`;
break;
case "railway":
link = `${window.location.origin}/integrations/railway/authorize`;
break;
case "hashicorp-vault":
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
break;
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
break;
default:
break;
}
if (link !== "") {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
export const redirectToIntegrationAppConfigScreen = (provider: string, integrationAuthId: string) =>
`/integrations/${provider}/create?integrationAuthId=${integrationAuthId}`;
@@ -0,0 +1,222 @@
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import NavHeader from "@app/components/navigation/NavHeader";
import { Button, Modal, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useDeleteIntegration,
useDeleteIntegrationAuth,
useGetCloudIntegrations,
useGetUserWsKey,
useGetWorkspaceAuthorizations,
useGetWorkspaceBot,
useGetWorkspaceIntegrations,
useUpdateBotActiveStatus
} from "@app/hooks/api";
import { IntegrationAuth } from "@app/hooks/api/types";
import { CloudIntegrationSection } from "./components/CloudIntegrationSection";
import { FrameworkIntegrationSection } from "./components/FrameworkIntegrationSection";
import { IntegrationsSection } from "./components/IntegrationsSection";
import {
generateBotKey,
redirectForProviderAuth,
redirectToIntegrationAppConfigScreen
} from "./IntegrationPage.utils";
type Props = {
frameworkIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
};
export const IntegrationsPage = ({ frameworkIntegrations }: Props) => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const router = useRouter();
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?._id || "";
const environments = currentWorkspace?.environments || [];
const { data: latestWsKey } = useGetUserWsKey(workspaceId);
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"activeBot"
] as const);
const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } =
useGetCloudIntegrations();
const { data: integrationAuths, isLoading: isIntegrationAuthLoading } =
useGetWorkspaceAuthorizations(
workspaceId,
useCallback((data: IntegrationAuth[]) => {
const groupBy: Record<string, IntegrationAuth> = {};
data.forEach((el) => {
groupBy[el.integration] = el;
});
return groupBy;
}, [])
);
// mutation
const {
data: integrations,
isLoading: isIntegrationLoading,
isFetching: isIntegrationFetching
} = useGetWorkspaceIntegrations(workspaceId);
const { data: bot } = useGetWorkspaceBot(workspaceId);
// mutation
const { mutateAsync: updateBotActiveStatus, mutate: updateBotActiveStatusSync } =
useUpdateBotActiveStatus();
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
const {
mutateAsync: deleteIntegrationAuth,
isLoading: isDeleteIntegrationAuthSuccess,
reset: resetDeleteIntegrationAuth
} = useDeleteIntegrationAuth();
// summary: this use effect is trigger when all integration auths are removed thus deactivate bot
// details: so onsuccessfully deleting an integration auth, immediately integration list is refeteched
// After the refetch is completed check if its empty. Then set bot active and reset the submit hook
useEffect(() => {
if (isDeleteIntegrationAuthSuccess && !isIntegrationFetching && !integrations?.length) {
if (bot?._id)
updateBotActiveStatusSync({
isActive: false,
botId: bot._id,
workspaceId
});
resetDeleteIntegrationAuth();
}
}, [isIntegrationFetching, isDeleteIntegrationAuthSuccess, integrations?.length]);
const handleProviderIntegration = async (provider: string) => {
const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug);
if (!selectedCloudIntegration) return;
try {
if (bot && !bot.isActive) {
const botKey = generateBotKey(bot.publicKey, latestWsKey!);
await updateBotActiveStatus({
workspaceId,
botKey,
isActive: true,
botId: bot._id
});
}
const integrationAuthForProvider = integrationAuths?.[provider];
if (!integrationAuthForProvider) {
redirectForProviderAuth(selectedCloudIntegration);
return;
}
const url = redirectToIntegrationAppConfigScreen(provider, integrationAuthForProvider._id);
router.push(url);
} catch (error) {
console.error(error);
}
};
// function to strat integration for a provider
// confirmation to user passing the bot key for provider to get secret access
const handleProviderIntegrationStart = (provider: string) => {
if (!bot?.isActive) {
handlePopUpOpen("activeBot", { provider });
return;
}
handleProviderIntegration(provider);
};
const handleUserAcceptBotCondition = () => {
const { provider } = popUp.activeBot?.data as { provider: string };
handleProviderIntegration(provider);
handlePopUpClose("activeBot");
};
const handleIntegrationDelete = async (integrationId: string, cb: () => void) => {
try {
await deleteIntegration({ id: integrationId, workspaceId });
if (cb) cb();
createNotification({
type: "success",
text: "Deleted integration"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to delete integration"
});
}
};
const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => {
const integrationAuthForProvider = integrationAuths?.[provider];
if (!integrationAuthForProvider) return;
try {
await deleteIntegrationAuth({
id: integrationAuthForProvider._id,
workspaceId
});
if (cb) cb();
createNotification({
type: "success",
text: "Revoked provider authentication"
});
} catch (err) {
console.error(err);
createNotification({
type: "error",
text: "Failed to revoke provider authentication"
});
}
};
return (
<div className="container mx-auto max-w-7xl px-8 pb-12 text-white">
<NavHeader pageName={t("integrations.title")} isProjectRelated />
<IntegrationsSection
isLoading={isIntegrationLoading}
integrations={integrations}
environments={environments}
onIntegrationDelete={({ _id: id }, cb) => handleIntegrationDelete(id, cb)}
/>
<CloudIntegrationSection
isLoading={isCloudIntegrationsLoading || isIntegrationAuthLoading}
cloudIntegrations={cloudIntegrations}
integrationAuths={integrationAuths}
onIntegrationStart={handleProviderIntegrationStart}
onIntegrationRevoke={handleIntegrationAuthRevoke}
/>
<Modal
isOpen={popUp.activeBot?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("activeBot", isOpen)}
>
<ModalContent
title={t("integrations.grant-access-to-secrets") as string}
footerContent={
<div className="flex items-center space-x-2">
<Button onClick={() => handleUserAcceptBotCondition()}>
{t("integrations.grant-access-button") as string}
</Button>
<Button
onClick={() => handlePopUpClose("activeBot")}
variant="outline_bg"
colorSchema="secondary"
>
Cancel
</Button>
</div>
}
>
{t("integrations.why-infisical-needs-access")}
</ModalContent>
</Modal>
<FrameworkIntegrationSection frameworks={frameworkIntegrations} />
</div>
);
};
@@ -0,0 +1,140 @@
import { useTranslation } from "react-i18next";
import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DeleteActionModal,Skeleton, Tooltip } from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { IntegrationAuth, TCloudIntegration } from "@app/hooks/api/types";
type Props = {
isLoading?: boolean;
integrationAuths?: Record<string, IntegrationAuth>;
cloudIntegrations?: TCloudIntegration[];
onIntegrationStart: (slug: string) => void;
// cb: handle popUpClose child->parent communication pattern
onIntegrationRevoke: (slug: string, cb: () => void) => void;
};
type TRevokeIntegrationPopUp = { provider: string };
export const CloudIntegrationSection = ({
isLoading,
cloudIntegrations = [],
integrationAuths = {},
onIntegrationStart,
onIntegrationRevoke
}: Props) => {
const { t } = useTranslation();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation"
] as const);
const isEmpty = !isLoading && !cloudIntegrations?.length;
return (
<div>
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">{t("integrations.cloud-integrations")}</h1>
<p className="text-base text-gray-400">{t("integrations.click-to-start")}</p>
</div>
<div
className="mx-6 grid grid-flow-dense gap-4"
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(256px, 1fr))" }}
>
{isLoading &&
Array.from({ length: 12 }).map((_, index) => (
<Skeleton className="h-32" key={`cloud-integration-skeleton-${index + 1}`} />
))}
{!isLoading &&
cloudIntegrations?.map((cloudIntegration) => (
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
className={`group relative ${
cloudIntegration.isAvailable
? "cursor-pointer duration-200 hover:bg-mineshaft-700"
: "opacity-50"
} flex h-32 flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4`}
onClick={() => {
if (!cloudIntegration.isAvailable) return;
onIntegrationStart(cloudIntegration.slug);
}}
key={cloudIntegration.slug}
>
<img
src={`/images/integrations/${cloudIntegration.image}`}
height={70}
width={70}
alt="integration logo"
/>
{cloudIntegration.name.split(" ").length > 2 ? (
<div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
<div>{cloudIntegration.name.split(" ")[0]}</div>
<div className="text-base">
{cloudIntegration.name.split(" ")[1]} {cloudIntegration.name.split(" ")[2]}
</div>
</div>
) : (
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
{cloudIntegration.name}
</div>
)}
{cloudIntegration.isAvailable &&
Boolean(integrationAuths?.[cloudIntegration.slug]) && (
<div className="absolute top-0 right-0 z-40 h-full">
<div className="relative h-full">
<div className="absolute top-0 right-0 w-24 flex-row items-center overflow-hidden whitespace-nowrap rounded-tr-md rounded-bl-md bg-primary py-0.5 px-2 text-xs text-black opacity-80 transition-all duration-300 group-hover:w-0 group-hover:p-0">
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
Authorized
</div>
<Tooltip content="Revoke Access">
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={async (event) => {
event.stopPropagation();
handlePopUpOpen("deleteConfirmation", {
provider: cloudIntegration.slug
});
}}
className="absolute top-0 right-0 flex h-0 w-12 cursor-pointer items-center justify-center overflow-hidden rounded-r-md bg-red text-xs opacity-50 transition-all duration-300 hover:opacity-100 group-hover:h-full"
>
<FontAwesomeIcon icon={faXmark} size="xl" />
</div>
</Tooltip>
</div>
</div>
)}
</div>
))}
</div>
{isEmpty && (
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
{Array.from({ length: 16 }).map((_, index) => (
<div
key={`dummy-cloud-integration-${index + 1}`}
className="h-32 animate-pulse rounded-md border border-mineshaft-600 bg-mineshaft-800"
/>
))}
</div>
)}
<DeleteActionModal
isOpen={popUp.deleteConfirmation.isOpen}
title={`Are you sure want to revoke access ${
(popUp?.deleteConfirmation.data as TRevokeIntegrationPopUp)?.provider || " "
}?`}
subTitle="This will remove all the secret integration of this provider!!!"
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
deleteKey={(popUp?.deleteConfirmation?.data as TRevokeIntegrationPopUp)?.provider || ""}
onDeleteApproved={async () => {
onIntegrationRevoke(
(popUp.deleteConfirmation.data as TRevokeIntegrationPopUp)?.provider,
() => handlePopUpClose("deleteConfirmation")
);
}}
/>
</div>
);
};
@@ -0,0 +1 @@
export { CloudIntegrationSection } from "./CloudIntegrationSection";
@@ -0,0 +1,54 @@
import { useTranslation } from "react-i18next";
type Props = {
frameworks: Array<{
name: string;
image: string;
slug: string;
docsLink: string;
}>;
};
export const FrameworkIntegrationSection = ({ frameworks }: Props) => {
const { t } = useTranslation();
return (
<>
<div className="mx-4 mt-12 mb-4 flex flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">{t("integrations.framework-integrations")}</h1>
<p className="text-base text-gray-400">{t("integrations.click-to-setup")}</p>
</div>
<div
className="mx-6 mt-4 grid grid-flow-dense gap-3"
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))" }}
>
{frameworks.map((framework) => (
<a
key={`framework-integration-${framework.slug}`}
href={framework.docsLink}
rel="noopener noreferrer"
target="_blank"
className="relative flex h-32 cursor-pointer flex-row items-center justify-center rounded-md p-0.5 duration-200"
>
<div
className={`flex h-full w-full cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 font-semibold text-gray-300 duration-200 hover:bg-mineshaft-700 group-hover:text-gray-200 ${
framework?.name?.split(" ").length > 1 ? "px-1 text-sm" : "px-2 text-xl"
} w-full max-w-xs text-center`}
>
{framework?.image && (
<img
src={`/images/integrations/${framework.image}.png`}
height={framework?.name ? 60 : 90}
width={framework?.name ? 60 : 90}
alt="integration logo"
/>
)}
{framework?.name && framework?.image && <div className="h-2" />}
{framework?.name && framework.name}
</div>
</a>
))}
</div>
</>
);
};
@@ -0,0 +1 @@
export { FrameworkIntegrationSection } from "./FrameworkIntegrationSection";
@@ -0,0 +1,149 @@
import { faArrowRight, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { integrationSlugNameMapping } from "public/data/frequentConstants";
import {
DeleteActionModal,
EmptyState,
FormControl,
FormLabel,
IconButton,
Select,
SelectItem,
Skeleton,
Tooltip
} from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { TIntegration } from "@app/hooks/api/types";
type Props = {
environments: Array<{ name: string; slug: string }>;
integrations?: TIntegration[];
isLoading?: boolean;
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
};
export const IntegrationsSection = ({
integrations = [],
environments = [],
isLoading,
onIntegrationDelete
}: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation"
] as const);
return (
<div className="mb-8">
<div className="mx-4 mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">Current Integrations</h1>
<p className="text-base text-bunker-300">Manage integrations with third-party services.</p>
</div>
{isLoading && (
<div className="p-6 pt-0">
<Skeleton className="h-28" />
</div>
)}
{!isLoading && !integrations.length && (
<EmptyState
className="mx-6 rounded-md border border-mineshaft-700 pt-8 pb-4"
title="No integrations found. Click on one of the below providers to sync secrets."
/>
)}
{!isLoading && (
<div className="flex flex-col space-y-4 p-6 pt-0">
{integrations?.map((integration) => (
<div
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 pb-2"
key={`integration-${integration?._id.toString()}`}
>
<div className="flex">
<div>
<FormControl label="Environment">
<Select
value={integration.environment}
isDisabled={integration.isActive}
className="min-w-[8rem] border border-mineshaft-700"
>
{environments.map((environment) => {
return (
<SelectItem
value={environment.slug}
key={`environment-${environment.slug}`}
>
{environment.name}
</SelectItem>
);
})}
</Select>
</FormControl>
</div>
<div className="ml-2 flex flex-col">
<FormLabel label="Secret Path" />
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.secretPath}
</div>
</div>
<div className="flex h-full items-center">
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400" />
</div>
<div className="ml-4 flex flex-col">
<FormLabel label="Integration" />
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integrationSlugNameMapping[integration.integration]}
</div>
</div>
<div className="ml-2 flex flex-col">
<FormLabel label="App" />
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.integration === "hashicorp-vault"
? `${integration.app} - path: ${integration.path}`
: integration.app}
</div>
</div>
{(integration.integration === "vercel" ||
integration.integration === "netlify" ||
integration.integration === "railway" ||
integration.integration === "gitlab") && (
<div className="ml-4 flex flex-col">
<FormLabel label="Target Environment" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.targetEnvironment}
</div>
</div>
)}
</div>
<div className="flex cursor-default items-center">
<div className="ml-2 opacity-80 duration-200 hover:opacity-100">
<Tooltip content="Remove Integration">
<IconButton
onClick={() => handlePopUpOpen("deleteConfirmation", integration)}
ariaLabel="delete"
colorSchema="danger"
variant="star"
>
<FontAwesomeIcon icon={faXmark} className="px-0.5" />
</IconButton>
</Tooltip>
</div>
</div>
</div>
))}
</div>
)}
<DeleteActionModal
isOpen={popUp.deleteConfirmation.isOpen}
title={`Are you sure want to remove ${
(popUp?.deleteConfirmation.data as TIntegration)?.integration || " "
} integration for ${(popUp?.deleteConfirmation.data as TIntegration)?.app || " "}?`}
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
deleteKey={(popUp?.deleteConfirmation?.data as TIntegration)?.app || ""}
onDeleteApproved={async () =>
onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () =>
handlePopUpClose("deleteConfirmation")
)
}
/>
</div>
);
};
@@ -0,0 +1 @@
export { IntegrationsSection } from "./IntegrationsSection";
@@ -0,0 +1 @@
export { IntegrationsPage } from "./IntegrationsPage";
@@ -0,0 +1,26 @@
import { useTranslation } from "react-i18next";
import NavHeader from "@app/components/navigation/NavHeader";
import {
BillingTabGroup
} from "./components";
export const BillingSettingsPage = () => {
const { t } = useTranslation();
return (
<div className="h-full py-8 px-4">
<NavHeader pageName={t("billing.title")} />
<div className="ml-4 flex text-3xl mt-8 items-start max-w-screen-lg">
<div className="flex-1">
<p className="font-semibold text-gray-200">{t("billing.title")}</p>
</div>
<div />
</div>
<div className="ml-4">
<BillingTabGroup />
</div>
</div>
);
};
@@ -0,0 +1,11 @@
import { CurrentPlanSection } from "./CurrentPlanSection";
import { PreviewSection } from "./PreviewSection";
export const BillingCloudTab = () => {
return (
<div>
<PreviewSection />
<CurrentPlanSection />
</div>
);
}
@@ -0,0 +1,87 @@
import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetOrgPlanTable
} from "@app/hooks/api";
export const CurrentPlanSection = () => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? "");
const displayCell = (value: null | number | string | boolean) => {
if (value === null) return "-";
if (typeof value === "boolean") {
if (value) return (
<FontAwesomeIcon
icon={faCircleCheck}
color='#2ecc71'
/>
);
return (
<FontAwesomeIcon
icon={faCircleXmark}
color='#e74c3c'
/>
);
}
return value;
}
return (
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
<h2 className="text-xl font-semibold flex-1 text-white mb-8">Current Usage</h2>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-1/3">Feature</Th>
<Th className="w-1/3">Allowed</Th>
<Th className="w-1/3">Used</Th>
</Tr>
</THead>
<TBody>
{!isLoading && data && data?.rows?.length > 0 && data.rows.map(({
name,
allowed,
used
}) => {
return (
<Tr key={`current-plan-row-${name}`} className="h-12">
<Td>{name}</Td>
<Td>{displayCell(allowed)}</Td>
<Td>{used}</Td>
</Tr>
);
})}
{isLoading && <TableSkeleton columns={5} key="invoices" />}
{!isLoading && data && data?.rows?.length === 0 && (
<Tr>
<Td colSpan={3}>
<EmptyState
title="No plan details found"
icon={faFileInvoice}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</div>
);
}
@@ -0,0 +1,64 @@
import { Fragment } from "react"
import { Tab } from "@headlessui/react"
import {
Modal,
ModalContent
} from "@app/components/v2";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { ManagePlansTable } from "./ManagePlansTable";
type Props = {
popUp: UsePopUpState<["managePlan"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["managePlan"]>, state?: boolean) => void;
};
export const ManagePlansModal = ({
popUp,
handlePopUpToggle
}: Props) => {
return (
<Modal
isOpen={popUp?.managePlan?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("managePlan", isOpen);
}}
>
<ModalContent className="max-w-screen-lg" title="Infisical Cloud Plans">
<Tab.Group>
<Tab.List className="border-b-2 border-mineshaft-600 max-w-screen-lg">
<Tab as={Fragment}>
{({ selected }) => (
<button
type="button"
className={`p-4 ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"} w-30 font-semibold outline-none`}
>
Bill monthly
</button>
)}
</Tab>
<Tab as={Fragment}>
{({ selected }) => (
<button
type="button"
className={`p-4 ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"} w-30 font-semibold outline-none`}
>
Bill yearly
</button>
)}
</Tab>
</Tab.List>
<Tab.Panels className="mt-4">
<Tab.Panel>
<ManagePlansTable billingCycle="monthly" />
</Tab.Panel>
<Tab.Panel>
<ManagePlansTable billingCycle="yearly" />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</ModalContent>
</Modal>
);
}
@@ -0,0 +1,176 @@
import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr,
} from "@app/components/v2";
import { useOrganization,useSubscription } from "@app/context";
import {
useCreateCustomerPortalSession,
useGetOrgPlansTable} from "@app/hooks/api";
type Props = {
billingCycle: "monthly" | "yearly"
}
export const ManagePlansTable = ({
billingCycle
}: Props) => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({
organizationId: currentOrg?._id ?? "",
billingCycle
});
const createCustomerPortalSession = useCreateCustomerPortalSession();
const displayCell = (value: null | number | string | boolean) => {
if (value === null) return "Unlimited";
if (typeof value === "boolean") {
if (value) return (
<FontAwesomeIcon
icon={faCircleCheck}
color='#2ecc71'
/>
);
return (
<FontAwesomeIcon
icon={faCircleXmark}
color='#e74c3c'
/>
);
}
return value;
}
return (
<TableContainer>
<Table>
<THead>
{subscription && !isTableDataLoading && tableData && (
<Tr>
<Th className="">Feature / Limit</Th>
{tableData.head.map(({
name,
priceLine
}) => {
return (
<Th
key={`plans-feature-head-${billingCycle}-${name}`}
className="text-center flex-1"
>
<p>{name}</p>
<p>{priceLine}</p>
</Th>
);
})}
</Tr>
)}
</THead>
<TBody>
{subscription && !isTableDataLoading && tableData && tableData.rows.map(({
name,
starter,
team,
pro,
enterprise
}) => {
return (
<Tr className="h-12" key={`plans-feature-row-${billingCycle}-${name}`}>
<Td>{displayCell(name)}</Td>
<Td className="text-center">
{displayCell(starter)}
</Td>
<Td className="text-center">
{displayCell(team)}
</Td>
<Td className="text-center">
{displayCell(pro)}
</Td>
<Td className="text-center">
{displayCell(enterprise)}
</Td>
</Tr>
);
})}
{isTableDataLoading && <TableSkeleton columns={5} key="cloud-products" />}
{!isTableDataLoading && tableData?.rows.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No cloud product details found"
icon={faFileInvoice}
/>
</Td>
</Tr>
)}
{subscription && !isTableDataLoading && tableData && (
<Tr className="h-12">
<Td />
{tableData.head.map(({
slug,
tier
}) => {
const isCurrentPlan = slug === subscription.slug;
let subscriptionText = "Upgrade";
if (subscription.tier > tier) {
subscriptionText = "Downgrade"
}
if (tier === 3) {
subscriptionText = "Contact sales"
}
return isCurrentPlan ? (
<Td>
<Button
colorSchema="secondary"
className="w-full"
isDisabled
>
Current
</Button>
</Td>
) : (
<Td>
<Button
onClick={async () => {
if (!currentOrg?._id) return;
if (tier !== 3) {
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id);
window.location.href = url;
return;
}
window.location.href = "https://infisical.com/scheduledemo";
}}
color="mineshaft"
className="w-full"
>
{subscriptionText}
</Button>
</Td>
);
})}
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
@@ -0,0 +1,100 @@
import { Button } from "@app/components/v2";
import { useOrganization,useSubscription } from "@app/context";
import {
useCreateCustomerPortalSession,
useGetOrgPlanBillingInfo} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { ManagePlansModal } from "./ManagePlansModal";
export const PreviewSection = () => {
const { currentOrg } = useOrganization();
const { subscription, isLoading: isSubscriptionLoading } = useSubscription();
const { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?._id ?? "");
const createCustomerPortalSession = useCreateCustomerPortalSession();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"managePlan"
] as const);
const formatAmount = (amount: number) => {
const formattedTotal = (Math.floor(amount) / 100).toLocaleString("en-US", {
style: "currency",
currency: "USD",
});
return formattedTotal;
}
const formatDate = (date: number) => {
const createdDate = new Date(date * 1000);
const day: number = createdDate.getDate();
const month: number = createdDate.getMonth() + 1;
const year: number = createdDate.getFullYear();
const formattedDate: string = `${day}/${month}/${year}`;
return formattedDate;
}
function formatPlanSlug(slug: string) {
return slug
.replace(/(\b[a-z])/g, match => match.toUpperCase())
.replace(/-/g, " ");
}
return (
<div>
{!isSubscriptionLoading && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && (
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mt-8 flex items-center bg-mineshaft-600 max-w-screen-lg">
<div className="flex-1">
<h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2>
<p className="text-gray-400 mt-4">Unlimited members, projects, RBAC, smart alerts, and so much more</p>
</div>
<Button
onClick={() => handlePopUpOpen("managePlan")}
color="mineshaft"
>
Upgrade
</Button>
</div>
)}
{!isLoading && data && subscription && (
<div className="flex mt-8 max-w-screen-lg">
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 mr-4 border border-mineshaft-600">
<p className="mb-2 text-gray-400">Current plan</p>
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
{formatPlanSlug(subscription.slug)}
</p>
<button
type="button"
onClick={async () => {
if (!currentOrg?._id) return;
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id);
window.location.href = url;
}}
className="text-primary"
>
Manage plan &rarr;
</button>
</div>
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mr-4">
<p className="mb-2 text-gray-400">Price</p>
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
{`${formatAmount(data.amount)} / ${data.interval}`}
</p>
</div>
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600">
<p className="mb-2 text-gray-400">Subscription renews on</p>
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
{formatDate(data.currentPeriodEnd)}
</p>
</div>
</div>
)}
<ManagePlansModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
</div>
);
}
@@ -0,0 +1 @@
export { BillingCloudTab } from "./BillingCloudTab";
@@ -0,0 +1,15 @@
import { CompanyNameSection } from "./CompanyNameSection";
import { InvoiceEmailSection } from "./InvoiceEmailSection";
import { PmtMethodsSection } from "./PmtMethodsSection";
import { TaxIDSection } from "./TaxIDSection";
export const BillingDetailsTab = () => {
return (
<>
<CompanyNameSection />
<InvoiceEmailSection />
<PmtMethodsSection />
<TaxIDSection />
</>
);
}
@@ -0,0 +1,96 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import Button from "@app/components/basic/buttons/Button";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { FormControl,Input } from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetOrgBillingDetails,
useUpdateOrgBillingDetails
} from "@app/hooks/api";
const schema = yup.object({
name: yup.string().required("Company name is required")
}).required();
export const CompanyNameSection = () => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { reset, control, handleSubmit } = useForm({
defaultValues: {
name: ""
},
resolver: yupResolver(schema)
});
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
const updateOrgBillingDetails = useUpdateOrgBillingDetails();
useEffect(() => {
if (data) {
reset({
name: data?.name ?? ""
});
}
}, [data]);
const onFormSubmit = async ({ name }: { name: string }) => {
try {
if (!currentOrg?._id) return;
if (name === "") return;
await updateOrgBillingDetails.mutateAsync({
name,
organizationId: currentOrg._id
});
createNotification({
text: "Successfully updated business name",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update business name",
type: "error"
});
}
}
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600"
>
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
Business name
</h2>
<div className="max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
placeholder="Acme Corp"
{...field}
className="bg-mineshaft-800"
/>
</FormControl>
)}
control={control}
name="name"
/>
</div>
<div className="inline-block">
<Button
text="Save"
type="submit"
color="mineshaft"
size="md"
onButtonPressed={() => console.log("Saved company name")}
/>
</div>
</form>
);
}
@@ -0,0 +1,99 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import Button from "@app/components/basic/buttons/Button";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
FormControl,
Input} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetOrgBillingDetails,
useUpdateOrgBillingDetails
} from "@app/hooks/api";
const schema = yup.object({
email: yup.string().required("Email is required")
}).required();
export const InvoiceEmailSection = () => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { reset, control, handleSubmit } = useForm({
defaultValues: {
email: ""
},
resolver: yupResolver(schema)
});
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
const updateOrgBillingDetails = useUpdateOrgBillingDetails();
useEffect(() => {
if (data) {
reset({
email: data?.email ?? ""
});
}
}, [data]);
const onFormSubmit = async ({ email }: { email: string }) => {
try {
if (!currentOrg?._id) return;
if (email === "") return;
await updateOrgBillingDetails.mutateAsync({
email,
organizationId: currentOrg._id
});
createNotification({
text: "Successfully updated invoice email recipient",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update invoice email recipient",
type: "error"
});
}
}
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600"
>
<h2 className="text-xl font-semibold flex-1 text-white mb-8">
Invoice email recipient
</h2>
<div className="max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
placeholder="[email protected]"
{...field}
className="bg-mineshaft-800"
/>
</FormControl>
)}
control={control}
name="email"
/>
</div>
<div className="inline-block">
<Button
text="Save"
type="submit"
color="mineshaft"
size="md"
onButtonPressed={() => console.log("Saved email address")}
/>
</div>
</form>
);
}
@@ -0,0 +1,44 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import Button from "@app/components/basic/buttons/Button";
import { useOrganization } from "@app/context";
import { useAddOrgPmtMethod } from "@app/hooks/api";
import { PmtMethodsTable } from "./PmtMethodsTable";
export const PmtMethodsSection = () => {
const { currentOrg } = useOrganization();
const addOrgPmtMethod = useAddOrgPmtMethod();
const handleAddPmtMethodBtnClick = async () => {
if (!currentOrg?._id) return;
const url = await addOrgPmtMethod.mutateAsync({
organizationId: currentOrg._id,
success_url: window.location.href,
cancel_url: window.location.href
});
window.location.href = url;
}
return (
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">
Payment Methods
</h2>
<div className="inline-block">
<Button
text="Add method"
type="submit"
color="mineshaft"
size="md"
icon={faPlus}
onButtonPressed={handleAddPmtMethodBtnClick}
/>
</div>
</div>
<PmtMethodsTable />
</div>
);
}
@@ -0,0 +1,90 @@
import { faCreditCard, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
EmptyState,
IconButton,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useDeleteOrgPmtMethod,
useGetOrgPmtMethods
} from "@app/hooks/api";
export const PmtMethodsTable = () => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgPmtMethods(currentOrg?._id ?? "");
const deleteOrgPmtMethod = useDeleteOrgPmtMethod();
const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => {
if (!currentOrg?._id) return;
await deleteOrgPmtMethod.mutateAsync({
organizationId: currentOrg._id,
pmtMethodId
});
}
return (
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="flex-1">Brand</Th>
<Th className="flex-1">Type</Th>
<Th className="flex-1">Last 4 Digits</Th>
<Th className="flex-1">Expiration</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isLoading && data && data?.length > 0 && data.map(({
_id,
brand,
exp_month,
exp_year,
funding,
last4
}) => (
<Tr key={`pmt-method-${_id}`} className="h-10">
<Td>{brand.charAt(0).toUpperCase() + brand.slice(1)}</Td>
<Td>{funding.charAt(0).toUpperCase() + funding.slice(1)}</Td>
<Td>{last4}</Td>
<Td>{`${exp_month}/${exp_year}`}</Td>
<Td>
<IconButton
onClick={async () => {
await handleDeletePmtMethodBtnClick(_id);
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
))}
{isLoading && <TableSkeleton columns={5} key="pmt-methods" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No payment methods on file"
icon={faCreditCard}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
@@ -0,0 +1,195 @@
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useAddOrgTaxId } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const taxIDTypes = [
{ label: "Australia ABN", value: "au_abn" },
{ label: "Australia ARN", value: "au_arn" },
{ label: "Bulgaria UIC", value: "bg_uic" },
{ label: "Brazil CNPJ", value: "br_cnpj" },
{ label: "Brazil CPF", value: "br_cpf" },
{ label: "Canada BN", value: "ca_bn" },
{ label: "Canada GST/HST", value: "ca_gst_hst" },
{ label: "Canada PST BC", value: "ca_pst_bc" },
{ label: "Canada PST MB", value: "ca_pst_mb" },
{ label: "Canada PST SK", value: "ca_pst_sk" },
{ label: "Canada QST", value: "ca_qst" },
{ label: "Switzerland VAT", value: "ch_vat" },
{ label: "Chile TIN", value: "cl_tin" },
{ label: "Egypt TIN", value: "eg_tin" },
{ label: "Spain CIF", value: "es_cif" },
{ label: "EU OSS VAT", value: "eu_oss_vat" },
{ label: "EU VAT", value: "eu_vat" },
{ label: "GB VAT", value: "gb_vat" },
{ label: "Georgia VAT", value: "ge_vat" },
{ label: "Hong Kong BR", value: "hk_br" },
{ label: "Hungary TIN", value: "hu_tin" },
{ label: "Indonesia NPWP", value: "id_npwp" },
{ label: "Israel VAT", value: "il_vat" },
{ label: "India GST", value: "in_gst" },
{ label: "Iceland VAT", value: "is_vat" },
{ label: "Japan CN", value: "jp_cn" },
{ label: "Japan RN", value: "jp_rn" },
{ label: "Japan TRN", value: "jp_trn" },
{ label: "Kenya PIN", value: "ke_pin" },
{ label: "South Korea BRN", value: "kr_brn" },
{ label: "Liechtenstein UID", value: "li_uid" },
{ label: "Mexico RFC", value: "mx_rfc" },
{ label: "Malaysia FRP", value: "my_frp" },
{ label: "Malaysia ITN", value: "my_itn" },
{ label: "Malaysia SST", value: "my_sst" },
{ label: "Norway VAT", value: "no_vat" },
{ label: "New Zealand GST", value: "nz_gst" },
{ label: "Philippines TIN", value: "ph_tin" },
{ label: "Russia INN", value: "ru_inn" },
{ label: "Russia KPP", value: "ru_kpp" },
{ label: "Saudi Arabia VAT", value: "sa_vat" },
{ label: "Singapore GST", value: "sg_gst" },
{ label: "Singapore UEN", value: "sg_uen" },
{ label: "Slovenia TIN", value: "si_tin" },
{ label: "Thailand VAT", value: "th_vat" },
{ label: "Turkey TIN", value: "tr_tin" },
{ label: "Taiwan VAT", value: "tw_vat" },
{ label: "Ukraine VAT", value: "ua_vat" },
{ label: "US EIN", value: "us_ein" },
{ label: "South Africa VAT", value: "za_vat" }
];
const schema = yup.object({
type: yup.string().required("Tax ID type is required"),
value: yup.string().required("Tax ID value is required")
}).required();
export type AddTaxIDFormData = yup.InferType<typeof schema>;
type Props = {
popUp: UsePopUpState<["addTaxID"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["addTaxID"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addTaxID"]>, state?: boolean) => void;
};
export const TaxIDModal = ({
popUp,
handlePopUpClose,
handlePopUpToggle
}: Props) => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const addOrgTaxId = useAddOrgTaxId();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<AddTaxIDFormData>({
resolver: yupResolver(schema)
});
const onTaxIDModalSubmit = async ({ type, value }: AddTaxIDFormData) => {
try {
if (!currentOrg?._id) return;
await addOrgTaxId.mutateAsync({
organizationId: currentOrg._id,
type,
value
});
createNotification({
text: "Successfully added Tax ID",
type: "success"
});
handlePopUpClose("addTaxID");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to add Tax ID",
type: "error"
});
}
}
return (
<Modal
isOpen={popUp?.addTaxID?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addTaxID", isOpen);
reset();
}}
>
<ModalContent title="Add Tax ID">
<form onSubmit={handleSubmit(onTaxIDModalSubmit)}>
<Controller
control={control}
name="type"
defaultValue="eu_vat"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Type"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{taxIDTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="value"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Value"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="DE000000000"
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Add
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
@@ -0,0 +1,39 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import Button from "@app/components/basic/buttons/Button";
import { usePopUp } from "@app/hooks/usePopUp";
import { TaxIDModal } from "./TaxIDModal";
import { TaxIDTable } from "./TaxIDTable";
export const TaxIDSection = () => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addTaxID"
] as const);
return (
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">
Tax ID
</h2>
<div className="inline-block">
<Button
text="Add Tax ID"
type="submit"
color="mineshaft"
size="md"
icon={faPlus}
onButtonPressed={() => handlePopUpOpen("addTaxID")}
/>
</div>
</div>
<TaxIDTable />
<TaxIDModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
</div>
);
}
@@ -0,0 +1,137 @@
import { faFileInvoice,faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
EmptyState,
IconButton,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr,
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useDeleteOrgTaxId,
useGetOrgTaxIds
} from "@app/hooks/api";
const taxIDTypeLabelMap: { [key: string]: string } = {
"au_abn": "Australia ABN",
"au_arn": "Australia ARN",
"bg_uic": "Bulgaria UIC",
"br_cnpj": "Brazil CNPJ",
"br_cpf": "Brazil CPF",
"ca_bn": "Canada BN",
"ca_gst_hst": "Canada GST/HST",
"ca_pst_bc": "Canada PST BC",
"ca_pst_mb": "Canada PST MB",
"ca_pst_sk": "Canada PST SK",
"ca_qst": "Canada QST",
"ch_vat": "Switzerland VAT",
"cl_tin": "Chile TIN",
"eg_tin": "Egypt TIN",
"es_cif": "Spain CIF",
"eu_oss_vat": "EU OSS VAT",
"eu_vat": "EU VAT",
"gb_vat": "GB VAT",
"ge_vat": "Georgia VAT",
"hk_br": "Hong Kong BR",
"hu_tin": "Hungary TIN",
"id_npwp": "Indonesia NPWP",
"il_vat": "Israel VAT",
"in_gst": "India GST",
"is_vat": "Iceland VAT",
"jp_cn": "Japan CN",
"jp_rn": "Japan RN",
"jp_trn": "Japan TRN",
"ke_pin": "Kenya PIN",
"kr_brn": "South Korea BRN",
"li_uid": "Liechtenstein UID",
"mx_rfc": "Mexico RFC",
"my_frp": "Malaysia FRP",
"my_itn": "Malaysia ITN",
"my_sst": "Malaysia SST",
"no_vat": "Norway VAT",
"nz_gst": "New Zealand GST",
"ph_tin": "Philippines TIN",
"ru_inn": "Russia INN",
"ru_kpp": "Russia KPP",
"sa_vat": "Saudi Arabia VAT",
"sg_gst": "Singapore GST",
"sg_uen": "Singapore UEN",
"si_tin": "Slovenia TIN",
"th_vat": "Thailand VAT",
"tr_tin": "Turkey TIN",
"tw_vat": "Taiwan VAT",
"ua_vat": "Ukraine VAT",
"us_ein": "US EIN",
"za_vat": "South Africa VAT"
};
export const TaxIDTable = () => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgTaxIds(currentOrg?._id ?? "");
const deleteOrgTaxId = useDeleteOrgTaxId();
const handleDeleteTaxIdBtnClick = async (taxId: string) => {
if (!currentOrg?._id) return;
await deleteOrgTaxId.mutateAsync({
organizationId: currentOrg._id,
taxId
});
}
return (
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="flex-1">Type</Th>
<Th className="flex-1">Value</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isLoading && data && data?.length > 0 && data.map(({
_id,
type,
value
}) => (
<Tr key={`tax-id-${_id}`} className="h-10">
<Td>{taxIDTypeLabelMap[type]}</Td>
<Td>{value}</Td>
<Td>
<IconButton
onClick={async () => {
await handleDeleteTaxIdBtnClick(_id);
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
))}
{isLoading && <TableSkeleton columns={3} key="tax-ids" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No Tax IDs on file"
icon={faFileInvoice}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
@@ -0,0 +1 @@
export { BillingDetailsTab } from "./BillingDetailsTab";
@@ -0,0 +1,10 @@
import { InvoicesTable } from "./InvoicesTable";
export const BillingReceiptsTab = () => {
return (
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
<h2 className="text-xl font-semibold flex-1 text-white">Invoices</h2>
<InvoicesTable />
</div>
);
}
@@ -0,0 +1,88 @@
import { faDownload, faFileInvoice } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
EmptyState,
IconButton,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetOrgInvoices
} from "@app/hooks/api";
export const InvoicesTable = () => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgInvoices(currentOrg?._id ?? "");
return (
<TableContainer className="mt-8">
<Table>
<THead>
<Tr>
<Th className="flex-1">Invoice #</Th>
<Th className="flex-1">Date</Th>
<Th className="flex-1">Status</Th>
<Th className="flex-1">Amount</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isLoading && data && data?.length > 0 && data.map(({
_id,
created,
paid,
number,
total,
invoice_pdf
}) => {
const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", {
style: "currency",
currency: "USD",
});
const createdDate = new Date(created * 1000);
const day: number = createdDate.getDate();
const month: number = createdDate.getMonth() + 1;
const year: number = createdDate.getFullYear();
const formattedDate: string = `${day}/${month}/${year}`;
return (
<Tr key={`invoice-${_id}`} className="h-10">
<Td>{number}</Td>
<Td>{formattedDate}</Td>
<Td>{paid ? "Paid" : "Not Paid"}</Td>
<Td>{formattedTotal}</Td>
<Td>
<IconButton
onClick={async () => window.open(invoice_pdf)}
size="lg"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Td>
</Tr>
);
})}
{isLoading && <TableSkeleton columns={5} key="invoices" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No invoices on file"
icon={faFileInvoice}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
@@ -0,0 +1 @@
export { BillingReceiptsTab } from "./BillingReceiptsTab";
@@ -0,0 +1,44 @@
import { Fragment } from "react"
import { Tab } from "@headlessui/react"
import { BillingCloudTab } from "../BillingCloudTab";
import { BillingDetailsTab } from "../BillingDetailsTab";
import { BillingReceiptsTab } from "../BillingReceiptsTab";
const tabs = [
{ name: "Infisical Cloud", key: "tab-infisical-cloud" },
{ name: "Receipts", key: "tab-receipts" },
{ name: "Billing details", key: "tab-billing-details" }
];
export const BillingTabGroup = () => {
return (
<Tab.Group>
<Tab.List className="mt-8 border-b-2 border-mineshaft-800 max-w-screen-lg">
{tabs.map((tab) => (
<Tab as={Fragment} key={tab.key}>
{({ selected }) => (
<button
type="button"
className={`w-30 p-4 font-semibold outline-none ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"}`}
>
{tab.name}
</button>
)}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<BillingCloudTab />
</Tab.Panel>
<Tab.Panel>
<BillingReceiptsTab />
</Tab.Panel>
<Tab.Panel>
<BillingDetailsTab />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
);
}
@@ -0,0 +1 @@
export { BillingTabGroup } from "./BillingTabGroup";
@@ -0,0 +1 @@
export { BillingTabGroup } from "./BillingTabGroup";
@@ -0,0 +1 @@
export { BillingSettingsPage } from "./BillingSettingsPage";
@@ -31,8 +31,6 @@ import {
} from "./components"; } from "./components";
export const OrgSettingsPage = () => { export const OrgSettingsPage = () => {
const host = window.location.origin;
const { t } = useTranslation(); const { t } = useTranslation();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
@@ -210,27 +208,6 @@ export const OrgSettingsPage = () => {
} }
}; };
/**
* This function deleted a workspace.
* It first checks if there is more than one workspace available. Otherwise, it doesn't delete
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
* It then deletes the workspace and forwards the user to another available workspace.
*/
// const executeDeletingWorkspace = async () => {
// const userWorkspaces = await getWorkspaces();
//
// if (userWorkspaces.length > 1) {
// if (
// userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
// workspaceToBeDeletedName
// ) {
// await deleteWorkspace(workspaceId);
// const ws = await getWorkspaces();
// router.push(`/dashboard/${ws[0]._id}`);
// }
// }
// };
//
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<NavHeader pageName={t("settings.org.title")} /> <NavHeader pageName={t("settings.org.title")} />
+1 -1
View File
@@ -18,4 +18,4 @@ version: 0.1.6
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
appVersion: "0.1.6" appVersion: "0.1.7"
@@ -127,7 +127,7 @@ func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKub
return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil
} }
func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) error { func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) error {
plainProcessedSecrets := make(map[string][]byte) plainProcessedSecrets := make(map[string][]byte)
for _, secret := range secretsFromAPI { for _, secret := range secretsFromAPI {
plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process
@@ -155,7 +155,7 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
return nil return nil
} }
func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) error { func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) error {
plainProcessedSecrets := make(map[string][]byte) plainProcessedSecrets := make(map[string][]byte)
for _, secret := range secretsFromAPI { for _, secret := range secretsFromAPI {
plainProcessedSecrets[secret.Key] = []byte(secret.Value) plainProcessedSecrets[secret.Key] = []byte(secret.Value)
@@ -208,7 +208,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
} }
var plainTextSecretsFromApi []model.SingleEnvironmentVariable var plainTextSecretsFromApi []model.SingleEnvironmentVariable
var fullEncryptedSecretsResponse api.GetEncryptedSecretsV2Response var fullEncryptedSecretsResponse api.GetEncryptedSecretsV3Response
if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" { if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" {
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceAccount(serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag) plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceAccount(serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag)
+18 -14
View File
@@ -47,36 +47,40 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta
return tokenDetailsResponse, nil return tokenDetailsResponse, nil
} }
func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) { func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Request) (GetEncryptedSecretsV3Response, error) {
var encryptedSecretsResponse GetEncryptedSecretsV2Response var secretsResponse GetEncryptedSecretsV3Response
createHttpRequest := httpClient.
httpRequest := httpClient.
R(). R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT_NAME).
SetHeader("If-None-Match", request.ETag).
SetQueryParam("environment", request.Environment). SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId). SetQueryParam("workspaceId", request.WorkspaceId)
SetResult(&encryptedSecretsResponse).
SetHeader("User-Agent", USER_AGENT_NAME)
createHttpRequest.SetHeader("If-None-Match", request.ETag) if request.SecretPath != "" {
httpRequest.SetQueryParam("secretPath", request.SecretPath)
}
response, err := createHttpRequest.Get(fmt.Sprintf("%v/v2/secrets", API_HOST_URL)) response, err := httpRequest.Get(fmt.Sprintf("%v/v3/secrets", API_HOST_URL))
if err != nil { if err != nil {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err) return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unable to complete api request [err=%s]", err)
} }
if response.IsError() { if response.IsError() {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%s]", response) return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
} }
if response.StatusCode() == 304 { if response.StatusCode() == 304 {
encryptedSecretsResponse.Modified = false secretsResponse.Modified = false
} else { } else {
encryptedSecretsResponse.Modified = true secretsResponse.Modified = true
} }
encryptedSecretsResponse.ETag = response.Header().Get("etag") secretsResponse.ETag = response.Header().Get("etag")
return encryptedSecretsResponse, nil return secretsResponse, nil
} }
func CallGetServiceTokenAccountDetailsV2(httpClient *resty.Client) (ServiceAccountDetailsResponse, error) { func CallGetServiceTokenAccountDetailsV2(httpClient *resty.Client) (ServiceAccountDetailsResponse, error) {
+30 -17
View File
@@ -28,30 +28,42 @@ type GetEncryptedWorkspaceKeyResponse struct {
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} }
type GetEncryptedSecretsV2Request struct { type GetEncryptedSecretsV3Request struct {
Environment string `json:"environment"` Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"` WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
ETag string `json:"etag,omitempty"` ETag string `json:"etag,omitempty"`
} }
type GetEncryptedSecretsV2Response struct { type GetEncryptedSecretsV3Response struct {
Secrets []struct { Secrets []struct {
ID string `json:"_id"` ID string `json:"_id"`
Version int `json:"version"` Version int `json:"version"`
Workspace string `json:"workspace"` Workspace string `json:"workspace"`
Type string `json:"type"` Type string `json:"type"`
Environment string `json:"environment"` Tags []struct {
SecretKeyCiphertext string `json:"secretKeyCiphertext"` ID string `json:"_id"`
SecretKeyIV string `json:"secretKeyIV"` Name string `json:"name"`
SecretKeyTag string `json:"secretKeyTag"` Slug string `json:"slug"`
SecretValueCiphertext string `json:"secretValueCiphertext"` Workspace string `json:"workspace"`
SecretValueIV string `json:"secretValueIV"` } `json:"tags"`
SecretValueTag string `json:"secretValueTag"` Environment string `json:"environment"`
CreatedAt time.Time `json:"createdAt"` SecretKeyCiphertext string `json:"secretKeyCiphertext"`
UpdatedAt time.Time `json:"updatedAt"` SecretKeyIV string `json:"secretKeyIV"`
User string `json:"user,omitempty"` SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
Algorithm string `json:"algorithm"`
KeyEncoding string `json:"keyEncoding"`
Folder string `json:"folder"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"secrets"` } `json:"secrets"`
Modified bool `json:"modified,omitempty"` Modified bool `json:"modified,omitempty"`
ETag string `json:"ETag,omitempty"` ETag string `json:"ETag,omitempty"`
} }
@@ -64,6 +76,7 @@ type GetServiceTokenDetailsResponse struct {
EncryptedKey string `json:"encryptedKey"` EncryptedKey string `json:"encryptedKey"`
Iv string `json:"iv"` Iv string `json:"iv"`
Tag string `json:"tag"` Tag string `json:"tag"`
SecretPath string `json:"secretPath"`
} }
type ServiceAccountDetailsResponse struct { type ServiceAccountDetailsResponse struct {
+21 -20
View File
@@ -48,10 +48,10 @@ func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsRe
return serviceTokenDetails, nil return serviceTokenDetails, nil
} }
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV2Response, error) { func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 { if len(serviceTokenParts) < 4 {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
} }
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2]) serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
@@ -63,32 +63,33 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient) serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to get service token details. [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
} }
encryptedSecretsResponse, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace, WorkspaceId: serviceTokenDetails.Workspace,
Environment: serviceTokenDetails.Environment, Environment: serviceTokenDetails.Environment,
ETag: etag, ETag: etag,
SecretPath: serviceTokenDetails.SecretPath,
}) })
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, err return nil, api.GetEncryptedSecretsV3Response{}, err
} }
decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag) decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err)
} }
plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV) plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decrypt the required workspace key") return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt the required workspace key")
} }
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse) plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
} }
return plainTextSecrets, encryptedSecretsResponse, nil return plainTextSecrets, encryptedSecretsResponse, nil
@@ -97,19 +98,19 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([
// Fetches plaintext secrets from an API endpoint using a service account. // Fetches plaintext secrets from an API endpoint using a service account.
// The function fetches the service account details and keys, decrypts the workspace key, fetches the encrypted secrets for the specified project and environment, and decrypts the secrets using the decrypted workspace key. // The function fetches the service account details and keys, decrypts the workspace key, fetches the encrypted secrets for the specified project and environment, and decrypts the secrets using the decrypted workspace key.
// Returns the plaintext secrets, encrypted secrets response, and any errors that occurred during the process. // Returns the plaintext secrets, encrypted secrets response, and any errors that occurred during the process.
func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV2Response, error) { func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) {
httpClient := resty.New() httpClient := resty.New()
httpClient.SetAuthToken(serviceAccountCreds.AccessKey). httpClient.SetAuthToken(serviceAccountCreds.AccessKey).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient) serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err)
} }
serviceAccountKeys, err := api.CallGetServiceAccountKeysV2(httpClient, api.GetServiceAccountKeysRequest{ServiceAccountId: serviceAccountDetails.ServiceAccount.ID}) serviceAccountKeys, err := api.CallGetServiceAccountKeysV2(httpClient, api.GetServiceAccountKeysRequest{ServiceAccountId: serviceAccountDetails.ServiceAccount.ID})
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err)
} }
// find key for requested project // find key for requested project
@@ -121,45 +122,45 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou
} }
if workspaceServiceAccountKey.ID == "" || workspaceServiceAccountKey.EncryptedKey == "" || workspaceServiceAccountKey.Nonce == "" || serviceAccountCreds.PublicKey == "" || serviceAccountCreds.PrivateKey == "" { if workspaceServiceAccountKey.ID == "" || workspaceServiceAccountKey.EncryptedKey == "" || workspaceServiceAccountKey.Nonce == "" || serviceAccountCreds.PublicKey == "" || serviceAccountCreds.PrivateKey == "" {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err)
} }
cipherText, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.EncryptedKey) cipherText, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.EncryptedKey)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode EncryptedKey secrets because [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode EncryptedKey secrets because [err=%v]", err)
} }
nonce, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.Nonce) nonce, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.Nonce)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode nonce secrets because [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode nonce secrets because [err=%v]", err)
} }
publickey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PublicKey) publickey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PublicKey)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PublicKey secrets because [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PublicKey secrets because [err=%v]", err)
} }
privateKey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PrivateKey) privateKey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PrivateKey)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PrivateKey secrets because [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PrivateKey secrets because [err=%v]", err)
} }
plainTextWorkspaceKey := crypto.DecryptAsymmetric(cipherText, nonce, publickey, privateKey) plainTextWorkspaceKey := crypto.DecryptAsymmetric(cipherText, nonce, publickey, privateKey)
encryptedSecretsResponse, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: projectId, WorkspaceId: projectId,
Environment: environmentName, Environment: environmentName,
ETag: etag, ETag: etag,
}) })
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err)
} }
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse) plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err)
} }
return plainTextSecrets, encryptedSecretsResponse, nil return plainTextSecrets, encryptedSecretsResponse, nil
@@ -194,7 +195,7 @@ func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV st
}, nil }, nil
} }
func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) ([]model.SingleEnvironmentVariable, error) { func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) ([]model.SingleEnvironmentVariable, error) {
plainTextSecrets := []model.SingleEnvironmentVariable{} plainTextSecrets := []model.SingleEnvironmentVariable{}
for _, secret := range encryptedSecretsResponse.Secrets { for _, secret := range encryptedSecretsResponse.Secrets {
// Decrypt key // Decrypt key