mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-02 20:25:47 +00:00
@@ -1,72 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import crypto from "crypto";
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
import { APIKeyData } from "../../models";
|
|
||||||
import { getSaltRounds } from "../../config";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return API key data for user with id [req.user_id]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getAPIKeyData = async (req: Request, res: Response) => {
|
|
||||||
const apiKeyData = await APIKeyData.find({
|
|
||||||
user: req.user._id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
apiKeyData,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new API key data for user with id [req.user._id]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const createAPIKeyData = async (req: Request, res: Response) => {
|
|
||||||
const { name, expiresIn } = req.body;
|
|
||||||
|
|
||||||
const secret = crypto.randomBytes(16).toString("hex");
|
|
||||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
|
||||||
|
|
||||||
const expiresAt = new Date();
|
|
||||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
|
||||||
|
|
||||||
let apiKeyData = await new APIKeyData({
|
|
||||||
name,
|
|
||||||
lastUsed: new Date(),
|
|
||||||
expiresAt,
|
|
||||||
user: req.user._id,
|
|
||||||
secretHash,
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
// return api key data without sensitive data
|
|
||||||
// FIX: fix this any
|
|
||||||
apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any;
|
|
||||||
|
|
||||||
if (!apiKeyData) throw new Error("Failed to find API key data");
|
|
||||||
|
|
||||||
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
apiKey,
|
|
||||||
apiKeyData,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete API key data with id [apiKeyDataId].
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteAPIKeyData = async (req: Request, res: Response) => {
|
|
||||||
const { apiKeyDataId } = req.params;
|
|
||||||
const apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
apiKeyData,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -4,7 +4,6 @@ import * as usersController from "./usersController";
|
|||||||
import * as organizationsController from "./organizationsController";
|
import * as organizationsController from "./organizationsController";
|
||||||
import * as workspaceController from "./workspaceController";
|
import * as workspaceController from "./workspaceController";
|
||||||
import * as serviceTokenDataController from "./serviceTokenDataController";
|
import * as serviceTokenDataController from "./serviceTokenDataController";
|
||||||
import * as apiKeyDataController from "./apiKeyDataController";
|
|
||||||
import * as secretController from "./secretController";
|
import * as secretController from "./secretController";
|
||||||
import * as secretsController from "./secretsController";
|
import * as secretsController from "./secretsController";
|
||||||
import * as serviceAccountsController from "./serviceAccountsController";
|
import * as serviceAccountsController from "./serviceAccountsController";
|
||||||
@@ -18,7 +17,6 @@ export {
|
|||||||
organizationsController,
|
organizationsController,
|
||||||
workspaceController,
|
workspaceController,
|
||||||
serviceTokenDataController,
|
serviceTokenDataController,
|
||||||
apiKeyDataController,
|
|
||||||
secretController,
|
secretController,
|
||||||
secretsController,
|
secretsController,
|
||||||
serviceAccountsController,
|
serviceAccountsController,
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const createWorkspaceTag = async (req: Request, res: Response) => {
|
|||||||
user: new Types.ObjectId(req.user._id),
|
user: new Types.ObjectId(req.user._id),
|
||||||
};
|
};
|
||||||
|
|
||||||
const createdTag = await new Tag(tagToCreate);
|
const createdTag = await new Tag(tagToCreate).save();
|
||||||
|
|
||||||
res.json(createdTag);
|
res.json(createdTag);
|
||||||
};
|
};
|
||||||
@@ -49,7 +49,11 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const getWorkspaceTags = async (req: Request, res: Response) => {
|
export const getWorkspaceTags = async (req: Request, res: Response) => {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const workspaceTags = await Tag.find({ workspace: workspaceId });
|
|
||||||
|
const workspaceTags = await Tag.find({
|
||||||
|
workspace: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
workspaceTags
|
workspaceTags
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
import {
|
import {
|
||||||
MembershipOrg,
|
MembershipOrg,
|
||||||
User,
|
User,
|
||||||
|
APIKeyData,
|
||||||
|
TokenVersion
|
||||||
} from "../../models";
|
} from "../../models";
|
||||||
|
import { getSaltRounds } from "../../config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the current user.
|
* Return the current user.
|
||||||
@@ -117,3 +123,106 @@ export const getMyOrganizations = async (req: Request, res: Response) => {
|
|||||||
organizations,
|
organizations,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return API keys belonging to current user.
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getMyAPIKeys = async (req: Request, res: Response) => {
|
||||||
|
const apiKeyData = await APIKeyData.find({
|
||||||
|
user: req.user._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send(apiKeyData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new API key for current user.
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createAPIKey = async (req: Request, res: Response) => {
|
||||||
|
const { name, expiresIn } = req.body;
|
||||||
|
|
||||||
|
const secret = crypto.randomBytes(16).toString("hex");
|
||||||
|
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||||
|
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||||
|
|
||||||
|
let apiKeyData = await new APIKeyData({
|
||||||
|
name,
|
||||||
|
lastUsed: new Date(),
|
||||||
|
expiresAt,
|
||||||
|
user: req.user._id,
|
||||||
|
secretHash,
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
// return api key data without sensitive data
|
||||||
|
apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any;
|
||||||
|
|
||||||
|
if (!apiKeyData) throw new Error("Failed to find API key data");
|
||||||
|
|
||||||
|
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
apiKey,
|
||||||
|
apiKeyData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete API key with id [apiKeyDataId] belonging to current user
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const deleteAPIKey = async (req: Request, res: Response) => {
|
||||||
|
const { apiKeyDataId } = req.params;
|
||||||
|
|
||||||
|
const apiKeyData = await APIKeyData.findOneAndDelete({
|
||||||
|
_id: new Types.ObjectId(apiKeyDataId),
|
||||||
|
user: req.user._id
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
apiKeyData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return active sessions (TokenVersion) belonging to user
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getMySessions = async (req: Request, res: Response) => {
|
||||||
|
const tokenVersions = await TokenVersion.find({
|
||||||
|
user: req.user._id
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send(tokenVersions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke all active sessions belong to user
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteMySessions = async (req: Request, res: Response) => {
|
||||||
|
await TokenVersion.updateMany({
|
||||||
|
user: req.user._id,
|
||||||
|
}, {
|
||||||
|
$inc: {
|
||||||
|
refreshVersion: 1,
|
||||||
|
accessVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: "Successfully revoked all sessions"
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -54,23 +54,20 @@ export const getSecretVersions = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
const { secretId, workspaceId, environment, folderId } = req.params;
|
const { secretId } = req.params;
|
||||||
|
|
||||||
const offset: number = parseInt(req.query.offset as string);
|
const offset: number = parseInt(req.query.offset as string);
|
||||||
const limit: number = parseInt(req.query.limit as string);
|
const limit: number = parseInt(req.query.limit as string);
|
||||||
|
|
||||||
const secretVersions = await SecretVersion.find({
|
const secretVersions = await SecretVersion.find({
|
||||||
secret: secretId,
|
secret: secretId
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
folder: folderId,
|
|
||||||
})
|
})
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.skip(offset)
|
.skip(offset)
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secretVersions,
|
secretVersions
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,7 +132,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
// validate secret version
|
// validate secret version
|
||||||
const oldSecretVersion = await SecretVersion.findOne({
|
const oldSecretVersion = await SecretVersion.findOne({
|
||||||
secret: secretId,
|
secret: secretId,
|
||||||
version,
|
version
|
||||||
}).select("+secretBlindIndex");
|
}).select("+secretBlindIndex");
|
||||||
|
|
||||||
if (!oldSecretVersion) throw new Error("Failed to find secret version");
|
if (!oldSecretVersion) throw new Error("Failed to find secret version");
|
||||||
@@ -154,7 +151,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretValueTag,
|
secretValueTag,
|
||||||
algorithm,
|
algorithm,
|
||||||
folder,
|
folder,
|
||||||
keyEncoding,
|
keyEncoding
|
||||||
} = oldSecretVersion;
|
} = oldSecretVersion;
|
||||||
|
|
||||||
// update secret
|
// update secret
|
||||||
@@ -162,7 +159,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretId,
|
secretId,
|
||||||
{
|
{
|
||||||
$inc: {
|
$inc: {
|
||||||
version: 1,
|
version: 1
|
||||||
},
|
},
|
||||||
workspace,
|
workspace,
|
||||||
type,
|
type,
|
||||||
@@ -177,10 +174,10 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretValueTag,
|
secretValueTag,
|
||||||
folderId: folder,
|
folderId: folder,
|
||||||
algorithm,
|
algorithm,
|
||||||
keyEncoding,
|
keyEncoding
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true,
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -204,17 +201,17 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretValueTag,
|
secretValueTag,
|
||||||
folder,
|
folder,
|
||||||
algorithm,
|
algorithm,
|
||||||
keyEncoding,
|
keyEncoding
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
// take secret snapshot
|
// take secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId: secret.workspace,
|
workspaceId: secret.workspace,
|
||||||
environment,
|
environment,
|
||||||
folderId: folder,
|
folderId: folder
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secret,
|
secret
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ import {
|
|||||||
secrets as v2SecretsRouter,
|
secrets as v2SecretsRouter,
|
||||||
serviceTokenData as v2ServiceTokenDataRouter,
|
serviceTokenData as v2ServiceTokenDataRouter,
|
||||||
serviceAccounts as v2ServiceAccountsRouter,
|
serviceAccounts as v2ServiceAccountsRouter,
|
||||||
apiKeyData as v2APIKeyDataRouter,
|
|
||||||
environment as v2EnvironmentRouter,
|
environment as v2EnvironmentRouter,
|
||||||
tags as v2TagsRouter,
|
tags as v2TagsRouter,
|
||||||
} from "./routes/v2";
|
} from "./routes/v2";
|
||||||
@@ -138,7 +137,6 @@ const main = async () => {
|
|||||||
app.use("/api/v2/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets
|
app.use("/api/v2/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets
|
||||||
app.use("/api/v2/service-token", v2ServiceTokenDataRouter);
|
app.use("/api/v2/service-token", v2ServiceTokenDataRouter);
|
||||||
app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
|
app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
|
||||||
app.use("/api/v2/api-key", v2APIKeyDataRouter);
|
|
||||||
|
|
||||||
// v3 routes (experimental)
|
// v3 routes (experimental)
|
||||||
app.use("/api/v3/auth", v3AuthRouter);
|
app.use("/api/v3/auth", v3AuthRouter);
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
const router = express.Router();
|
|
||||||
import { body, param } from "express-validator";
|
|
||||||
import {
|
|
||||||
requireAuth,
|
|
||||||
validateRequest,
|
|
||||||
} from "../../middleware";
|
|
||||||
import { apiKeyDataController } from "../../controllers/v2";
|
|
||||||
import {
|
|
||||||
AUTH_MODE_JWT,
|
|
||||||
} from "../../variables";
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
"/",
|
|
||||||
requireAuth({
|
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
|
||||||
}),
|
|
||||||
apiKeyDataController.getAPIKeyData
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
"/",
|
|
||||||
requireAuth({
|
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
|
||||||
}),
|
|
||||||
body("name").exists().trim(),
|
|
||||||
body("expiresIn"), // measured in ms
|
|
||||||
validateRequest,
|
|
||||||
apiKeyDataController.createAPIKeyData
|
|
||||||
);
|
|
||||||
|
|
||||||
router.delete(
|
|
||||||
"/:apiKeyDataId",
|
|
||||||
requireAuth({
|
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
|
||||||
}),
|
|
||||||
param("apiKeyDataId").exists().trim(),
|
|
||||||
validateRequest,
|
|
||||||
apiKeyDataController.deleteAPIKeyData
|
|
||||||
);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -7,7 +7,6 @@ import secret from "./secret"; // deprecated
|
|||||||
import secrets from "./secrets";
|
import secrets from "./secrets";
|
||||||
import serviceTokenData from "./serviceTokenData";
|
import serviceTokenData from "./serviceTokenData";
|
||||||
import serviceAccounts from "./serviceAccounts";
|
import serviceAccounts from "./serviceAccounts";
|
||||||
import apiKeyData from "./apiKeyData";
|
|
||||||
import environment from "./environment"
|
import environment from "./environment"
|
||||||
import tags from "./tags"
|
import tags from "./tags"
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@ export {
|
|||||||
secrets,
|
secrets,
|
||||||
serviceTokenData,
|
serviceTokenData,
|
||||||
serviceAccounts,
|
serviceAccounts,
|
||||||
apiKeyData,
|
|
||||||
environment,
|
environment,
|
||||||
tags,
|
tags,
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
requireAuth,
|
requireAuth,
|
||||||
validateRequest,
|
validateRequest,
|
||||||
} from "../../middleware";
|
} from "../../middleware";
|
||||||
import { body } from "express-validator";
|
import { body, param } from "express-validator";
|
||||||
import { usersController } from "../../controllers/v2";
|
import { usersController } from "../../controllers/v2";
|
||||||
import {
|
import {
|
||||||
AUTH_MODE_API_KEY,
|
AUTH_MODE_API_KEY,
|
||||||
@@ -37,4 +37,49 @@ router.get(
|
|||||||
usersController.getMyOrganizations
|
usersController.getMyOrganizations
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/me/api-keys",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
}),
|
||||||
|
usersController.getMyAPIKeys
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/me/api-keys",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
}),
|
||||||
|
body("name").exists().isString().trim(),
|
||||||
|
body("expiresIn").isNumeric(),
|
||||||
|
validateRequest,
|
||||||
|
usersController.createAPIKey
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
"/me/api-keys/:apiKeyDataId",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
}),
|
||||||
|
param("apiKeyDataId").exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
usersController.deleteAPIKey
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get( // new
|
||||||
|
"/me/sessions",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
}),
|
||||||
|
usersController.getMySessions
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete( // new
|
||||||
|
"/me/sessions",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
}),
|
||||||
|
usersController.deleteMySessions
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
# The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used
|
# The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used
|
||||||
hostAPI: https://app.infisical.com/api
|
hostAPI: https://app.infisical.com/api
|
||||||
|
resyncInterval: 60 # <-- the time in seconds between secret re-sync. Faster re-syncs will require higher rate limits
|
||||||
authentication:
|
authentication:
|
||||||
serviceToken:
|
serviceToken:
|
||||||
serviceTokenSecretReference:
|
serviceTokenSecretReference:
|
||||||
@@ -79,6 +80,11 @@ spec:
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="resyncInterval">
|
||||||
|
This property defines the time in seconds between each secret re-sync from Infisical. Shorter time between re-syncs will require higher rate limits only available on paid plans.
|
||||||
|
Default re-sync interval is every 1 minute.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="authentication">
|
<Accordion title="authentication">
|
||||||
The `authentication` property tells the operator where it should look to find credentials needed to fetch secrets from Infisical.
|
The `authentication` property tells the operator where it should look to find credentials needed to fetch secrets from Infisical.
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Generated
+4493
-3451
File diff suppressed because it is too large
Load Diff
+13
-13
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
import { Fragment, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
|
|
||||||
import addAPIKey from "@app/pages/api/apiKey/addAPIKey";
|
|
||||||
|
|
||||||
import Button from "../buttons/Button";
|
|
||||||
import InputField from "../InputField";
|
|
||||||
import ListBox from "../Listbox";
|
|
||||||
|
|
||||||
const expiryMapping = {
|
|
||||||
"1 day": 86400,
|
|
||||||
"7 days": 604800,
|
|
||||||
"1 month": 2592000,
|
|
||||||
"6 months": 15552000,
|
|
||||||
"12 months": 31104000
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isOpen: boolean;
|
|
||||||
closeModal: () => void;
|
|
||||||
// TODO: These never and any will be filled by single folder that contains types and hooks about the API
|
|
||||||
apiKeys: any[];
|
|
||||||
setApiKeys: (arg: any[]) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: convert to TS
|
|
||||||
const AddApiKeyDialog = ({ isOpen, closeModal, apiKeys, setApiKeys }: Props) => {
|
|
||||||
const [apiKey, setApiKey] = useState("");
|
|
||||||
const [apiKeyName, setApiKeyName] = useState("");
|
|
||||||
const [apiKeyExpiresIn, setApiKeyExpiresIn] = useState("1 day");
|
|
||||||
const [apiKeyCopied, setApiKeyCopied] = useState(false);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const generateAPIKey = async () => {
|
|
||||||
const newApiKey = await addAPIKey({
|
|
||||||
name: apiKeyName,
|
|
||||||
expiresIn: expiryMapping[apiKeyExpiresIn as keyof typeof expiryMapping]
|
|
||||||
});
|
|
||||||
|
|
||||||
setApiKeys([...apiKeys, newApiKey.apiKeyData]);
|
|
||||||
setApiKey(newApiKey.apiKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
function copyToClipboard() {
|
|
||||||
// Get the text field
|
|
||||||
const copyText = document.getElementById("apiKey") as HTMLInputElement;
|
|
||||||
|
|
||||||
// Select the text field
|
|
||||||
copyText.select();
|
|
||||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
|
||||||
|
|
||||||
// Copy the text inside the text field
|
|
||||||
navigator.clipboard.writeText(copyText.value);
|
|
||||||
|
|
||||||
setApiKeyCopied(true);
|
|
||||||
setTimeout(() => setApiKeyCopied(false), 2000);
|
|
||||||
// Alert the copied text
|
|
||||||
// alert("Copied the text: " + copyText.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeAddApiKeyModal = () => {
|
|
||||||
closeModal();
|
|
||||||
setApiKeyName("");
|
|
||||||
setApiKey("");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="z-50">
|
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
|
||||||
<Dialog as="div" className="relative" onClose={closeModal}>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-bunker-700 bg-opacity-80" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 scale-95"
|
|
||||||
enterTo="opacity-100 scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 scale-100"
|
|
||||||
leaveTo="opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
{apiKey === "" ? (
|
|
||||||
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
|
||||||
<Dialog.Title
|
|
||||||
as="h3"
|
|
||||||
className="z-50 text-lg font-medium leading-6 text-gray-400"
|
|
||||||
>
|
|
||||||
{t("section.api-key.add-dialog.title")}
|
|
||||||
</Dialog.Title>
|
|
||||||
<div className="mt-2 mb-4">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
{t("section.api-key.add-dialog.description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-2 max-h-28">
|
|
||||||
<InputField
|
|
||||||
label={t("section.api-key.add-dialog.name")}
|
|
||||||
onChangeHandler={setApiKeyName}
|
|
||||||
type="varName"
|
|
||||||
value={apiKeyName}
|
|
||||||
placeholder=""
|
|
||||||
isRequired
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-28">
|
|
||||||
<ListBox
|
|
||||||
isSelected={apiKeyExpiresIn}
|
|
||||||
onChange={setApiKeyExpiresIn}
|
|
||||||
data={["1 day", "7 days", "1 month", "6 months", "12 months"]}
|
|
||||||
isFull
|
|
||||||
text={`${t("common.expired-in")}: `}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="max-w-max">
|
|
||||||
<div className="mt-6 flex w-max flex-col justify-start">
|
|
||||||
<Button
|
|
||||||
onButtonPressed={() => generateAPIKey()}
|
|
||||||
color="mineshaft"
|
|
||||||
text={t("section.api-key.add-dialog.add") as string}
|
|
||||||
textDisabled={t("section.api-key.add-dialog.add") as string}
|
|
||||||
size="md"
|
|
||||||
active={apiKeyName !== ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
) : (
|
|
||||||
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
|
||||||
<Dialog.Title
|
|
||||||
as="h3"
|
|
||||||
className="z-50 text-lg font-medium leading-6 text-gray-400"
|
|
||||||
>
|
|
||||||
{t("section.api-key.add-dialog.copy-service-token")}
|
|
||||||
</Dialog.Title>
|
|
||||||
<div className="mt-2 mb-4">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
{t("section.api-key.add-dialog.copy-service-token-description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="mt-2 mr-2 flex h-20 w-full items-center justify-end rounded-md bg-white/[0.07] text-base text-gray-400">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={apiKey}
|
|
||||||
disabled
|
|
||||||
id="apiKey"
|
|
||||||
className="invisible w-full min-w-full bg-white/0 py-2 px-2 text-gray-400 outline-none"
|
|
||||||
/>
|
|
||||||
<div className="w-full max-w-md break-words bg-white/0 py-2 pl-14 pr-2 text-sm text-gray-400 outline-none">
|
|
||||||
{apiKey}
|
|
||||||
</div>
|
|
||||||
<div className="group relative inline-block h-full font-normal text-gray-400 underline duration-200 hover:text-primary">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
className="h-full border-l border-white/20 py-2 pl-3.5 pr-4 duration-200 hover:bg-white/[0.12]"
|
|
||||||
>
|
|
||||||
{apiKeyCopied ? (
|
|
||||||
<FontAwesomeIcon icon={faCheck} className="pr-0.5" />
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon icon={faCopy} />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-chicago-900 px-3 py-2 text-center text-sm text-gray-400 duration-300 group-hover:flex group-hover:animate-popup">
|
|
||||||
{t("common.click-to-copy")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 flex w-max flex-col justify-start">
|
|
||||||
<Button
|
|
||||||
onButtonPressed={() => closeAddApiKeyModal()}
|
|
||||||
color="mineshaft"
|
|
||||||
text="Close"
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
)}
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddApiKeyDialog;
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import { faX } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
|
||||||
|
|
||||||
import deleteAPIKey from "../../../pages/api/apiKey/deleteAPIKey";
|
|
||||||
import guidGenerator from "../../utilities/randomId";
|
|
||||||
import Button from "../buttons/Button";
|
|
||||||
|
|
||||||
interface TokenProps {
|
|
||||||
_id: string;
|
|
||||||
name: string;
|
|
||||||
expiresAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ServiceTokensProps {
|
|
||||||
data: TokenProps[];
|
|
||||||
setApiKeys: (value: TokenProps[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the component that we utilize for the api key table
|
|
||||||
* @param {object} obj
|
|
||||||
* @param {any[]} obj.data - current state of the api key table
|
|
||||||
* @param {function} obj.setApiKeys - updating the state of the api key table
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const ApiKeyTable = ({ data, setApiKeys }: ServiceTokensProps) => {
|
|
||||||
const { createNotification } = useNotificationContext();
|
|
||||||
return (
|
|
||||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
|
||||||
<div className="absolute rounded-t-md w-full h-12 bg-white/5" />
|
|
||||||
<table className="w-full my-1">
|
|
||||||
<thead className="text-bunker-300 text-sm font-light">
|
|
||||||
<tr>
|
|
||||||
<th className="text-left pl-6 pt-2.5 pb-2">API KEY NAME</th>
|
|
||||||
<th className="text-left pl-6 pt-2.5 pb-2">VALID UNTIL</th>
|
|
||||||
<th aria-label="button" />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{data?.length > 0 ? (
|
|
||||||
data?.map((row) => (
|
|
||||||
<tr
|
|
||||||
key={guidGenerator()}
|
|
||||||
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
|
|
||||||
>
|
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
|
||||||
{row.name}
|
|
||||||
</td>
|
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
|
||||||
{new Date(row.expiresAt).toUTCString()}
|
|
||||||
</td>
|
|
||||||
<td className="py-2 border-mineshaft-700 border-t">
|
|
||||||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
|
|
||||||
<Button
|
|
||||||
onButtonPressed={() => {
|
|
||||||
deleteAPIKey({ apiKeyId: row._id });
|
|
||||||
setApiKeys(data.filter((token) => token._id !== row._id));
|
|
||||||
createNotification({
|
|
||||||
text: `'${row.name}' API key has been revoked.`,
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
color="red"
|
|
||||||
size="icon-sm"
|
|
||||||
icon={faX}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">
|
|
||||||
No API keys yet
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ApiKeyTable;
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from "@fortawesome/free-solid-svg-icons";
|
import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { plans } from "public/data/frequentConstants";
|
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { Select, SelectItem } from "@app/components/v2";
|
import { Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { useSubscription } from "@app/context";
|
||||||
import updateUserProjectPermission from "@app/ee/api/memberships/UpdateUserProjectPermission";
|
import updateUserProjectPermission from "@app/ee/api/memberships/UpdateUserProjectPermission";
|
||||||
import getOrganizationSubscriptions from "@app/pages/api/organization/GetOrgSubscription";
|
|
||||||
import changeUserRoleInWorkspace from "@app/pages/api/workspace/changeUserRoleInWorkspace";
|
import changeUserRoleInWorkspace from "@app/pages/api/workspace/changeUserRoleInWorkspace";
|
||||||
import deleteUserFromWorkspace from "@app/pages/api/workspace/deleteUserFromWorkspace";
|
import deleteUserFromWorkspace from "@app/pages/api/workspace/deleteUserFromWorkspace";
|
||||||
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
|
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
|
||||||
@@ -40,13 +39,12 @@ type EnvironmentProps = {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoading }: Props) => {
|
const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoading }: Props) => {
|
||||||
|
const { subscription } = useSubscription();
|
||||||
const [roleSelected, setRoleSelected] = useState(
|
const [roleSelected, setRoleSelected] = useState(
|
||||||
Array(userData?.length).fill(userData.map((user) => user.role))
|
Array(userData?.length).fill(userData.map((user) => user.role))
|
||||||
);
|
);
|
||||||
const host = window.location.origin;
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [myRole, setMyRole] = useState("member");
|
const [myRole, setMyRole] = useState("member");
|
||||||
const [currentPlan, setCurrentPlan] = useState("");
|
|
||||||
const [workspaceEnvs, setWorkspaceEnvs] = useState<EnvironmentProps[]>([]);
|
const [workspaceEnvs, setWorkspaceEnvs] = useState<EnvironmentProps[]>([]);
|
||||||
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
|
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
@@ -128,7 +126,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
|
|||||||
denials = [];
|
denials = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPlan !== plans.professional && host === "https://app.infisical.com" && workspaceId !== "63ea8121b6e2b0543ba79616") {
|
if (subscription?.rbac === false) {
|
||||||
setIsUpgradeModalOpen(true);
|
setIsUpgradeModalOpen(true);
|
||||||
} else {
|
} else {
|
||||||
const allDenials = userData[index].deniedPermissions
|
const allDenials = userData[index].deniedPermissions
|
||||||
@@ -167,14 +165,6 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
|
|||||||
(async () => {
|
(async () => {
|
||||||
const result = await getProjectInfo({ projectId: workspaceId });
|
const result = await getProjectInfo({ projectId: workspaceId });
|
||||||
setWorkspaceEnvs(result.environments);
|
setWorkspaceEnvs(result.environments);
|
||||||
|
|
||||||
const orgId = localStorage.getItem("orgData.id") as string;
|
|
||||||
const subscriptions = await getOrganizationSubscriptions({
|
|
||||||
orgId
|
|
||||||
});
|
|
||||||
if (subscriptions) {
|
|
||||||
setCurrentPlan(subscriptions.data[0].plan.product);
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
}, [userData, myUser]);
|
}, [userData, myUser]);
|
||||||
|
|
||||||
@@ -208,11 +198,13 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
|
|||||||
return (
|
return (
|
||||||
<div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-600 bg-bunker">
|
<div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-600 bg-bunker">
|
||||||
<div className="absolute h-[3.1rem] w-full rounded-t-md bg-white/5" />
|
<div className="absolute h-[3.1rem] w-full rounded-t-md bg-white/5" />
|
||||||
<UpgradePlanModal
|
{subscription && (
|
||||||
isOpen={isUpgradeModalOpen}
|
<UpgradePlanModal
|
||||||
onClose={closeUpgradeModal}
|
isOpen={isUpgradeModalOpen}
|
||||||
text="You can change user permissions if you switch to Infisical's Professional plan."
|
onClose={closeUpgradeModal}
|
||||||
/>
|
text={subscription.slug === null ? "You can use RBAC under an Enterprise license" : "You can use RBAC if you switch to Infisical's Team Plan."}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<table className="my-0.5 w-full">
|
<table className="my-0.5 w-full">
|
||||||
<thead className="text-xs font-light text-gray-400 bg-mineshaft-800">
|
<thead className="text-xs font-light text-gray-400 bg-mineshaft-800">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import StripeRedirect from "@app/pages/api/organization/StripeRedirect";
|
|
||||||
|
|
||||||
import { tempLocalStorage } from "../utilities/checks/tempLocalStorage";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
plan: {
|
|
||||||
name: string;
|
|
||||||
price: string;
|
|
||||||
priceExplanation?: string;
|
|
||||||
text: string;
|
|
||||||
subtext?: string;
|
|
||||||
buttonTextMain: string;
|
|
||||||
buttonTextSecondary: string;
|
|
||||||
current: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Plan({ plan }: Props) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`relative flex flex-col justify-between border min-w-fit w-96 rounded-lg h-68 mr-4 bg-mineshaft-800 ${
|
|
||||||
plan.name !== "Starter" && plan.current === true ? "border-2 border-primary" : "border-mineshaft-600"
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="flex flex-row justify-between items-center relative z-10">
|
|
||||||
<p className="px-6 py-4 text-3xl font-semibold text-gray-400">{plan.name}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flwx-row items-end justify-start mb-4">
|
|
||||||
<p className="pl-6 text-3xl font-semibold text-primary">{plan.price}</p>
|
|
||||||
<p className="pl-3 mb-1 text-lg text-gray-400">{plan.priceExplanation}</p>
|
|
||||||
</div>
|
|
||||||
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.text}</p>
|
|
||||||
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.subtext}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
{plan.current === false ? (
|
|
||||||
<>
|
|
||||||
{plan.buttonTextMain === "Schedule a Demo" ? (
|
|
||||||
<a href="/scheduledemo" target='_blank rel="noopener"'>
|
|
||||||
<div className="relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-mineshaft-600 hover:text-black hover:border-primary text-gray-400 font-semibold hover:bg-primary bg-bunker duration-200 cursor-pointer rounded-md flex w-max">
|
|
||||||
{plan.buttonTextMain}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-mineshaft-600 text-gray-400 font-semibold ${
|
|
||||||
plan.buttonTextMain === "Downgrade"
|
|
||||||
? "hover:bg-red hover:text-white hover:border-red"
|
|
||||||
: "hover:bg-primary hover:text-black hover:border-primary"
|
|
||||||
} bg-bunker duration-200 cursor-pointer rounded-md flex w-max`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
StripeRedirect({
|
|
||||||
orgId: tempLocalStorage("orgData.id")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{plan.buttonTextMain}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<a href="https://infisical.com/pricing" target='_blank rel="noopener"'>
|
|
||||||
<div className="relative z-10 text-gray-400 font-semibold hover:text-primary duration-200 cursor-pointer mb-0.5">
|
|
||||||
{plan.buttonTextSecondary}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`h-8 w-full rounded-b-md flex justify-center items-center z-10 ${
|
|
||||||
plan.name !== "Starter" && plan.current === true ? "bg-primary" : "bg-mineshaft-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="text-xs text-black font-semibold">CURRENT PLAN</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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="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>
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
/* eslint-disable new-cap */
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import jsrp from "jsrp";
|
||||||
|
|
||||||
|
import changePassword2 from "@app/pages/api/auth/ChangePassword2";
|
||||||
|
import SRP1 from "@app/pages/api/auth/SRP1";
|
||||||
|
|
||||||
|
import Aes256Gcm from "./cryptography/aes-256-gcm";
|
||||||
|
import { deriveArgonKey } from "./cryptography/crypto";
|
||||||
|
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
||||||
|
|
||||||
|
const clientOldPassword = new jsrp.client();
|
||||||
|
const clientNewPassword = new jsrp.client();
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
email: string;
|
||||||
|
currentPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
clientOldPassword.init({ username: email, password: currentPassword }, async () => {
|
||||||
|
let serverPublicKey; let salt;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const clientPublicKey = clientOldPassword.getPublicKey();
|
||||||
|
|
||||||
|
const res = await SRP1({ clientPublicKey });
|
||||||
|
|
||||||
|
serverPublicKey = res.serverPublicKey;
|
||||||
|
salt = res.salt;
|
||||||
|
|
||||||
|
clientOldPassword.setSalt(salt);
|
||||||
|
clientOldPassword.setServerPublicKey(serverPublicKey);
|
||||||
|
|
||||||
|
const clientProof = clientOldPassword.getProof();
|
||||||
|
|
||||||
|
clientNewPassword.init({ username: email, password: newPassword }, async () => {
|
||||||
|
clientNewPassword.createVerifier(async (err, result) => {
|
||||||
|
try {
|
||||||
|
const derivedKey = await deriveArgonKey({
|
||||||
|
password: newPassword,
|
||||||
|
salt: result.salt,
|
||||||
|
mem: 65536,
|
||||||
|
time: 3,
|
||||||
|
parallelism: 1,
|
||||||
|
hashLen: 32
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||||
|
|
||||||
|
const key = crypto.randomBytes(32);
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
} = Aes256Gcm.encrypt({
|
||||||
|
text: localStorage.getItem("PRIVATE_KEY") as string,
|
||||||
|
secret: key
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: protectedKey,
|
||||||
|
iv: protectedKeyIV,
|
||||||
|
tag: protectedKeyTag
|
||||||
|
} = Aes256Gcm.encrypt({
|
||||||
|
text: key.toString("hex"),
|
||||||
|
secret: Buffer.from(derivedKey.hash)
|
||||||
|
});
|
||||||
|
|
||||||
|
await changePassword2({
|
||||||
|
clientProof,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt: result.salt,
|
||||||
|
verifier: result.verifier
|
||||||
|
});
|
||||||
|
|
||||||
|
saveTokenToLocalStorage({
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
} catch (err2) {
|
||||||
|
console.error(err2);
|
||||||
|
reject(err2);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default attemptChangePassword;
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export {
|
export {
|
||||||
useGetAuthToken,
|
useGetAuthToken,
|
||||||
useGetCommonPasswords,
|
useGetCommonPasswords,
|
||||||
useRevokeAllSessions,
|
useSendMfaToken,
|
||||||
useSendMfaToken,
|
useVerifyMfaToken
|
||||||
useVerifyMfaToken} from "./queries"
|
} from "./queries"
|
||||||
|
|||||||
@@ -51,15 +51,6 @@ export const useGetAuthToken = () =>
|
|||||||
retry: 0
|
retry: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useRevokeAllSessions = () => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
const { data } = await apiRequest.delete("/api/v1/auth/sessions");
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchCommonPasswords = async () => {
|
const fetchCommonPasswords = async () => {
|
||||||
const { data } = await apiRequest.get("/api/v1/auth/common-passwords");
|
const { data } = await apiRequest.get("/api/v1/auth/common-passwords");
|
||||||
return data || [];
|
return data || [];
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { useGetWorkspaceBot, useUpdateBotActiveStatus } from "./queries";
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
}
|
||||||
@@ -81,76 +81,79 @@ export const useGetProjectSecrets = ({
|
|||||||
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
||||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
|
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
|
||||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
|
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
|
||||||
select: useCallback((data: EncryptedSecret[]) => {
|
select: useCallback(
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
(data: EncryptedSecret[]) => {
|
||||||
const latestKey = decryptFileKey;
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
const key = decryptAssymmetric({
|
const latestKey = decryptFileKey;
|
||||||
ciphertext: latestKey.encryptedKey,
|
const key = decryptAssymmetric({
|
||||||
nonce: latestKey.nonce,
|
ciphertext: latestKey.encryptedKey,
|
||||||
publicKey: latestKey.sender.publicKey,
|
nonce: latestKey.nonce,
|
||||||
privateKey: PRIVATE_KEY
|
publicKey: latestKey.sender.publicKey,
|
||||||
});
|
privateKey: PRIVATE_KEY
|
||||||
|
|
||||||
const sharedSecrets: DecryptedSecret[] = [];
|
|
||||||
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
|
||||||
// this used for add-only mode in dashboard
|
|
||||||
// type won't be there thus only one key is shown
|
|
||||||
const duplicateSecretKey: Record<string, boolean> = {};
|
|
||||||
data.forEach((encSecret: EncryptedSecret) => {
|
|
||||||
const secretKey = decryptSymmetric({
|
|
||||||
ciphertext: encSecret.secretKeyCiphertext,
|
|
||||||
iv: encSecret.secretKeyIV,
|
|
||||||
tag: encSecret.secretKeyTag,
|
|
||||||
key
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretValue = decryptSymmetric({
|
const sharedSecrets: DecryptedSecret[] = [];
|
||||||
ciphertext: encSecret.secretValueCiphertext,
|
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||||
iv: encSecret.secretValueIV,
|
// this used for add-only mode in dashboard
|
||||||
tag: encSecret.secretValueTag,
|
// type won't be there thus only one key is shown
|
||||||
key
|
const duplicateSecretKey: Record<string, boolean> = {};
|
||||||
});
|
data.forEach((encSecret: EncryptedSecret) => {
|
||||||
|
const secretKey = decryptSymmetric({
|
||||||
|
ciphertext: encSecret.secretKeyCiphertext,
|
||||||
|
iv: encSecret.secretKeyIV,
|
||||||
|
tag: encSecret.secretKeyTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
const secretComment = decryptSymmetric({
|
const secretValue = decryptSymmetric({
|
||||||
ciphertext: encSecret.secretCommentCiphertext,
|
ciphertext: encSecret.secretValueCiphertext,
|
||||||
iv: encSecret.secretCommentIV,
|
iv: encSecret.secretValueIV,
|
||||||
tag: encSecret.secretCommentTag,
|
tag: encSecret.secretValueTag,
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptedSecret = {
|
const secretComment = decryptSymmetric({
|
||||||
_id: encSecret._id,
|
ciphertext: encSecret.secretCommentCiphertext,
|
||||||
env: encSecret.environment,
|
iv: encSecret.secretCommentIV,
|
||||||
key: secretKey,
|
tag: encSecret.secretCommentTag,
|
||||||
value: secretValue,
|
key
|
||||||
tags: encSecret.tags,
|
});
|
||||||
comment: secretComment,
|
|
||||||
createdAt: encSecret.createdAt,
|
|
||||||
updatedAt: encSecret.updatedAt
|
|
||||||
};
|
|
||||||
|
|
||||||
if (encSecret.type === "personal") {
|
const decryptedSecret = {
|
||||||
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
_id: encSecret._id,
|
||||||
id: encSecret._id,
|
env: encSecret.environment,
|
||||||
value: secretValue
|
key: secretKey,
|
||||||
|
value: secretValue,
|
||||||
|
tags: encSecret.tags,
|
||||||
|
comment: secretComment,
|
||||||
|
createdAt: encSecret.createdAt,
|
||||||
|
updatedAt: encSecret.updatedAt
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
if (encSecret.type === "personal") {
|
||||||
sharedSecrets.push(decryptedSecret);
|
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
||||||
|
id: encSecret._id,
|
||||||
|
value: secretValue
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
||||||
|
sharedSecrets.push(decryptedSecret);
|
||||||
|
}
|
||||||
|
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
||||||
}
|
}
|
||||||
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
});
|
||||||
}
|
sharedSecrets.forEach((val) => {
|
||||||
});
|
const dupKey = `${val.key}-${val.env}`;
|
||||||
sharedSecrets.forEach((val) => {
|
if (personalSecrets?.[dupKey]) {
|
||||||
const dupKey = `${val.key}-${val.env}`;
|
val.idOverride = personalSecrets[dupKey].id;
|
||||||
if (personalSecrets?.[dupKey]) {
|
val.valueOverride = personalSecrets[dupKey].value;
|
||||||
val.idOverride = personalSecrets[dupKey].id;
|
val.overrideAction = "modified";
|
||||||
val.valueOverride = personalSecrets[dupKey].value;
|
}
|
||||||
val.overrideAction = "modified";
|
});
|
||||||
}
|
return { secrets: sharedSecrets };
|
||||||
});
|
},
|
||||||
return { secrets: sharedSecrets };
|
[decryptFileKey]
|
||||||
}, [decryptFileKey])
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useGetProjectSecretsByKey = ({
|
export const useGetProjectSecretsByKey = ({
|
||||||
@@ -167,82 +170,85 @@ export const useGetProjectSecretsByKey = ({
|
|||||||
// right now secretpath is passed as folderid as only this is used in overview
|
// right now secretpath is passed as folderid as only this is used in overview
|
||||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
|
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
|
||||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
|
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
|
||||||
select: useCallback((data: EncryptedSecret[]) => {
|
select: useCallback(
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
(data: EncryptedSecret[]) => {
|
||||||
const latestKey = decryptFileKey;
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
const key = decryptAssymmetric({
|
const latestKey = decryptFileKey;
|
||||||
ciphertext: latestKey.encryptedKey,
|
const key = decryptAssymmetric({
|
||||||
nonce: latestKey.nonce,
|
ciphertext: latestKey.encryptedKey,
|
||||||
publicKey: latestKey.sender.publicKey,
|
nonce: latestKey.nonce,
|
||||||
privateKey: PRIVATE_KEY
|
publicKey: latestKey.sender.publicKey,
|
||||||
});
|
privateKey: PRIVATE_KEY
|
||||||
|
|
||||||
const sharedSecrets: Record<string, DecryptedSecret[]> = {};
|
|
||||||
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
|
||||||
// this used for add-only mode in dashboard
|
|
||||||
// type won't be there thus only one key is shown
|
|
||||||
const duplicateSecretKey: Record<string, boolean> = {};
|
|
||||||
const uniqSecKeys: Record<string, boolean> = {};
|
|
||||||
data.forEach((encSecret: EncryptedSecret) => {
|
|
||||||
const secretKey = decryptSymmetric({
|
|
||||||
ciphertext: encSecret.secretKeyCiphertext,
|
|
||||||
iv: encSecret.secretKeyIV,
|
|
||||||
tag: encSecret.secretKeyTag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
|
|
||||||
|
|
||||||
const secretValue = decryptSymmetric({
|
|
||||||
ciphertext: encSecret.secretValueCiphertext,
|
|
||||||
iv: encSecret.secretValueIV,
|
|
||||||
tag: encSecret.secretValueTag,
|
|
||||||
key
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretComment = decryptSymmetric({
|
const sharedSecrets: Record<string, DecryptedSecret[]> = {};
|
||||||
ciphertext: encSecret.secretCommentCiphertext,
|
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||||
iv: encSecret.secretCommentIV,
|
// this used for add-only mode in dashboard
|
||||||
tag: encSecret.secretCommentTag,
|
// type won't be there thus only one key is shown
|
||||||
key
|
const duplicateSecretKey: Record<string, boolean> = {};
|
||||||
});
|
const uniqSecKeys: Record<string, boolean> = {};
|
||||||
|
data.forEach((encSecret: EncryptedSecret) => {
|
||||||
|
const secretKey = decryptSymmetric({
|
||||||
|
ciphertext: encSecret.secretKeyCiphertext,
|
||||||
|
iv: encSecret.secretKeyIV,
|
||||||
|
tag: encSecret.secretKeyTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
|
||||||
|
|
||||||
const decryptedSecret = {
|
const secretValue = decryptSymmetric({
|
||||||
_id: encSecret._id,
|
ciphertext: encSecret.secretValueCiphertext,
|
||||||
env: encSecret.environment,
|
iv: encSecret.secretValueIV,
|
||||||
key: secretKey,
|
tag: encSecret.secretValueTag,
|
||||||
value: secretValue,
|
key
|
||||||
tags: encSecret.tags,
|
});
|
||||||
comment: secretComment,
|
|
||||||
createdAt: encSecret.createdAt,
|
|
||||||
updatedAt: encSecret.updatedAt
|
|
||||||
};
|
|
||||||
|
|
||||||
if (encSecret.type === "personal") {
|
const secretComment = decryptSymmetric({
|
||||||
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
ciphertext: encSecret.secretCommentCiphertext,
|
||||||
id: encSecret._id,
|
iv: encSecret.secretCommentIV,
|
||||||
value: secretValue
|
tag: encSecret.secretCommentTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedSecret = {
|
||||||
|
_id: encSecret._id,
|
||||||
|
env: encSecret.environment,
|
||||||
|
key: secretKey,
|
||||||
|
value: secretValue,
|
||||||
|
tags: encSecret.tags,
|
||||||
|
comment: secretComment,
|
||||||
|
createdAt: encSecret.createdAt,
|
||||||
|
updatedAt: encSecret.updatedAt
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
if (encSecret.type === "personal") {
|
||||||
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
|
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
||||||
sharedSecrets[secretKey].push(decryptedSecret);
|
id: encSecret._id,
|
||||||
}
|
value: secretValue
|
||||||
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
};
|
||||||
}
|
} else {
|
||||||
});
|
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
||||||
Object.keys(sharedSecrets).forEach((secName) => {
|
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
|
||||||
sharedSecrets[secName].forEach((val) => {
|
sharedSecrets[secretKey].push(decryptedSecret);
|
||||||
const dupKey = `${val.key}-${val.env}`;
|
}
|
||||||
if (personalSecrets?.[dupKey]) {
|
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
||||||
val.idOverride = personalSecrets[dupKey].id;
|
|
||||||
val.valueOverride = personalSecrets[dupKey].value;
|
|
||||||
val.overrideAction = "modified";
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
Object.keys(sharedSecrets).forEach((secName) => {
|
||||||
|
sharedSecrets[secName].forEach((val) => {
|
||||||
|
const dupKey = `${val.key}-${val.env}`;
|
||||||
|
if (personalSecrets?.[dupKey]) {
|
||||||
|
val.idOverride = personalSecrets[dupKey].id;
|
||||||
|
val.valueOverride = personalSecrets[dupKey].value;
|
||||||
|
val.overrideAction = "modified";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
|
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
|
||||||
}, [decryptFileKey])
|
},
|
||||||
|
[decryptFileKey]
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
||||||
@@ -263,29 +269,32 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
|
|||||||
enabled: Boolean(dto.secretId && dto.decryptFileKey),
|
enabled: Boolean(dto.secretId && dto.decryptFileKey),
|
||||||
queryKey: secretKeys.getSecretVersion(dto.secretId),
|
queryKey: secretKeys.getSecretVersion(dto.secretId),
|
||||||
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
|
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
|
||||||
select: useCallback((data: EncryptedSecretVersion[]) => {
|
select: useCallback(
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
(data: EncryptedSecretVersion[]) => {
|
||||||
const latestKey = dto.decryptFileKey;
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
const key = decryptAssymmetric({
|
const latestKey = dto.decryptFileKey;
|
||||||
ciphertext: latestKey.encryptedKey,
|
const key = decryptAssymmetric({
|
||||||
nonce: latestKey.nonce,
|
ciphertext: latestKey.encryptedKey,
|
||||||
publicKey: latestKey.sender.publicKey,
|
nonce: latestKey.nonce,
|
||||||
privateKey: PRIVATE_KEY
|
publicKey: latestKey.sender.publicKey,
|
||||||
});
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
return data
|
return data
|
||||||
.map((el) => ({
|
.map((el) => ({
|
||||||
createdAt: el.createdAt,
|
createdAt: el.createdAt,
|
||||||
id: el._id,
|
id: el._id,
|
||||||
value: decryptSymmetric({
|
value: decryptSymmetric({
|
||||||
ciphertext: el.secretValueCiphertext,
|
ciphertext: el.secretValueCiphertext,
|
||||||
iv: el.secretValueIV,
|
iv: el.secretValueIV,
|
||||||
tag: el.secretValueTag,
|
tag: el.secretValueTag,
|
||||||
key
|
key
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||||
}, [])
|
},
|
||||||
|
[dto.decryptFileKey]
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useBatchSecretsOp = () => {
|
export const useBatchSecretsOp = () => {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ export {
|
|||||||
fetchOrgUsers,
|
fetchOrgUsers,
|
||||||
useAddUserToOrg,
|
useAddUserToOrg,
|
||||||
useAddUserToWs,
|
useAddUserToWs,
|
||||||
|
useCreateAPIKey,
|
||||||
|
useDeleteAPIKey,
|
||||||
useDeleteOrgMembership,
|
useDeleteOrgMembership,
|
||||||
|
useGetMyAPIKeys,
|
||||||
|
useGetMySessions,
|
||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
useGetUser,
|
useGetUser,
|
||||||
useGetUserAction,
|
useGetUserAction,
|
||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useRegisterUserAction,
|
useRegisterUserAction,
|
||||||
useUpdateOrgUserRole
|
useRevokeMySessions,
|
||||||
} from "./queries";
|
useUpdateOrgUserRole} from "./queries";
|
||||||
|
|||||||
@@ -12,16 +12,20 @@ import {
|
|||||||
AddUserToOrgDTO,
|
AddUserToOrgDTO,
|
||||||
AddUserToWsDTO,
|
AddUserToWsDTO,
|
||||||
AddUserToWsRes,
|
AddUserToWsRes,
|
||||||
|
APIKeyData,
|
||||||
|
CreateAPIKeyRes,
|
||||||
DeletOrgMembershipDTO,
|
DeletOrgMembershipDTO,
|
||||||
OrgUser,
|
OrgUser,
|
||||||
|
TokenVersion,
|
||||||
UpdateOrgUserRoleDTO,
|
UpdateOrgUserRoleDTO,
|
||||||
User
|
User} from "./types";
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
const userKeys = {
|
const userKeys = {
|
||||||
getUser: ["user"] as const,
|
getUser: ["user"] as const,
|
||||||
userAction: ["user-action"] as const,
|
userAction: ["user-action"] as const,
|
||||||
getOrgUsers: (orgId: string) => [{ orgId }, "user"]
|
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
|
||||||
|
myAPIKeys: ["api-keys"] as const,
|
||||||
|
mySessions: ["sessions"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchUserDetails = async () => {
|
export const fetchUserDetails = async () => {
|
||||||
@@ -167,3 +171,90 @@ export const useLogoutUser = () =>
|
|||||||
localStorage.setItem("PRIVATE_KEY", "");
|
localStorage.setItem("PRIVATE_KEY", "");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useGetMyAPIKeys = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: userKeys.myAPIKeys,
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<APIKeyData[]>(
|
||||||
|
"/api/v2/users/me/api-keys"
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCreateAPIKey = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
name,
|
||||||
|
expiresIn
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
expiresIn: number;
|
||||||
|
}) => {
|
||||||
|
const { data } = await apiRequest.post<CreateAPIKeyRes>(
|
||||||
|
"/api/v2/users/me/api-keys",
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
expiresIn
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
queryClient.invalidateQueries(userKeys.myAPIKeys);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteAPIKey = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (apiKeyDataId: string) => {
|
||||||
|
const { data } = await apiRequest.delete(
|
||||||
|
`/api/v2/users/me/api-keys/${apiKeyDataId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
queryClient.invalidateQueries(userKeys.myAPIKeys);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetMySessions = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: userKeys.mySessions,
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TokenVersion[]>(
|
||||||
|
"/api/v2/users/me/sessions"
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRevokeMySessions = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
console.log("useRevokeAllSessions 1");
|
||||||
|
const { data } = await apiRequest.delete(
|
||||||
|
"/api/v2/users/me/sessions"
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("useRevokeAllSessions 2: ", data);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
queryClient.invalidateQueries(userKeys.mySessions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -61,3 +61,27 @@ export type AddUserToOrgDTO = {
|
|||||||
inviteeEmail: string;
|
inviteeEmail: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateAPIKeyRes = {
|
||||||
|
apiKey: string;
|
||||||
|
apiKeyData: APIKeyData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type APIKeyData = {
|
||||||
|
_id: string;
|
||||||
|
name: string;
|
||||||
|
user: string;
|
||||||
|
lastUsed: string;
|
||||||
|
createdAt: string;
|
||||||
|
expiresAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TokenVersion = {
|
||||||
|
_id: string;
|
||||||
|
user: string;
|
||||||
|
userAgent: string;
|
||||||
|
ip: string;
|
||||||
|
lastUsed: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
@@ -6,8 +6,10 @@ export {
|
|||||||
useGetUserWorkspaceMemberships,
|
useGetUserWorkspaceMemberships,
|
||||||
useGetUserWorkspaces,
|
useGetUserWorkspaces,
|
||||||
useGetUserWsEnvironments,
|
useGetUserWsEnvironments,
|
||||||
|
useGetWorkspaceAuthorizations,
|
||||||
useGetWorkspaceById,
|
useGetWorkspaceById,
|
||||||
useGetWorkspaceIndexStatus,
|
useGetWorkspaceIndexStatus,
|
||||||
|
useGetWorkspaceIntegrations,
|
||||||
useGetWorkspaceSecrets,
|
useGetWorkspaceSecrets,
|
||||||
useNameWorkspaceSecrets,
|
useNameWorkspaceSecrets,
|
||||||
useRenameWorkspace,
|
useRenameWorkspace,
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { useLeaveConfirm } from "./useLeaveConfirm";
|
export { useLeaveConfirm } from "./useLeaveConfirm";
|
||||||
export { usePersistentState } from "./usePersistentState";
|
export { usePersistentState } from "./usePersistentState";
|
||||||
export { usePopUp } from "./usePopUp";
|
export { usePopUp } from "./usePopUp";
|
||||||
|
export { useSyntaxHighlight } from "./useSyntaxHighlight";
|
||||||
export { useToggle } from "./useToggle";
|
export { useToggle } from "./useToggle";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { faCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
const REGEX = /([$]{.*?})/g;
|
||||||
|
|
||||||
|
export const useSyntaxHighlight = () => {
|
||||||
|
const syntaxHighlight = useCallback((text: string, isHidden?: boolean) => {
|
||||||
|
if (isHidden) {
|
||||||
|
return text
|
||||||
|
.split("")
|
||||||
|
.slice(0, 200)
|
||||||
|
.map((el, i) =>
|
||||||
|
el === "\n" ? (
|
||||||
|
el
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
key={`${text}_${el}_${i + 1}`}
|
||||||
|
className="mr-0.5 text-xxs"
|
||||||
|
icon={faCircle}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// append a space on last new line this to show new line in ui for code component
|
||||||
|
const val = text.at(-1) === "\n" ? text.concat(" ") : text;
|
||||||
|
if (val?.length === 0) return <span className="font-mono text-bunker-400/80">EMPTY</span>;
|
||||||
|
return val?.split(REGEX).map((word, i) =>
|
||||||
|
word.match(REGEX) !== null ? (
|
||||||
|
<span className="ph-no-capture text-yellow" key={`${val}-${i + 1}`}>
|
||||||
|
${
|
||||||
|
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span key={`${word}_${i + 1}`} className="ph-no-capture">
|
||||||
|
{word}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return syntaxHighlight;
|
||||||
|
};
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -158,7 +158,9 @@ export default function Activity() {
|
|||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
<meta property="og:image" content="/images/message.png" />
|
<meta property="og:image" content="/images/message.png" />
|
||||||
</Head>
|
</Head>
|
||||||
<NavHeader pageName="Audit Logs" isProjectRelated />
|
<div className="ml-6">
|
||||||
|
<NavHeader pageName="Audit Logs" isProjectRelated />
|
||||||
|
</div>
|
||||||
{currentSidebarAction && (
|
{currentSidebarAction && (
|
||||||
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
|
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
|
||||||
)}
|
)}
|
||||||
@@ -184,11 +186,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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
name: string;
|
|
||||||
expiresIn: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This route adds an API key for the user
|
|
||||||
* @param {object} obj
|
|
||||||
* @param {string} obj.name - name of the API key
|
|
||||||
* @param {string} obj.expiresIn - how soon the API key expires in ms
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const addAPIKey = ({ name, expiresIn }: Props) =>
|
|
||||||
SecurityClient.fetchCall("/api/v2/api-key/", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
name,
|
|
||||||
expiresIn
|
|
||||||
})
|
|
||||||
}).then(async (res) => {
|
|
||||||
if (res && res.status === 200) {
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
console.log("Failed to add API key");
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
export default addAPIKey;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
apiKeyId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This route revokes the API key with id [apiKeyId]
|
|
||||||
* @param {object} obj
|
|
||||||
* @param {string} obj.apiKeyId - id of the API key to delete
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const deleteAPIKey = ({ apiKeyId }: Props) =>
|
|
||||||
SecurityClient.fetchCall(`/api/v2/api-key/${apiKeyId}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
}).then(async (res) => {
|
|
||||||
if (res && res.status === 200) {
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
console.log("Failed to delete API key");
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
export default deleteAPIKey;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This route gets API keys for the user
|
|
||||||
* @param {*} param0
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const getAPIKeys = () =>
|
|
||||||
SecurityClient.fetchCall("/api/v2/api-key", {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
}).then(async (res) => {
|
|
||||||
if (res && res.status === 200) {
|
|
||||||
return (await res.json()).apiKeyData;
|
|
||||||
}
|
|
||||||
console.log("Failed to get API keys");
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
export default getAPIKeys;
|
|
||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,322 +1,18 @@
|
|||||||
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 { faBan,faCheck, faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import Button from "@app/components/basic/buttons/Button";
|
import { PersonalSettingsPage } from "@app/views/Settings/PersonalSettingsPage";
|
||||||
import InputField from "@app/components/basic/InputField";
|
|
||||||
import ListBox from "@app/components/basic/Listbox";
|
|
||||||
import ApiKeyTable from "@app/components/basic/table/ApiKeyTable";
|
|
||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
|
||||||
import checkPassword from "@app/components/utilities/checks/checkPassword";
|
|
||||||
import changePassword from "@app/components/utilities/cryptography/changePassword";
|
|
||||||
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
|
|
||||||
import {
|
|
||||||
useGetCommonPasswords,
|
|
||||||
useRevokeAllSessions} from "@app/hooks/api";
|
|
||||||
import { SecuritySection } from "@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection";
|
|
||||||
|
|
||||||
import AddApiKeyDialog from "../../../components/basic/dialog/AddApiKeyDialog";
|
|
||||||
import getAPIKeys from "../../api/apiKey/getAPIKeys";
|
|
||||||
import getUser from "../../api/user/getUser";
|
|
||||||
|
|
||||||
type Errors = {
|
|
||||||
length?: string,
|
|
||||||
upperCase?: string,
|
|
||||||
lowerCase?: string,
|
|
||||||
number?: string,
|
|
||||||
specialChar?: string,
|
|
||||||
repeatedChar?: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PersonalSettings() {
|
export default function PersonalSettings() {
|
||||||
const { data: commonPasswords } = useGetCommonPasswords();
|
const { t } = useTranslation();
|
||||||
const [personalEmail, setPersonalEmail] = useState("");
|
|
||||||
const [personalName, setPersonalName] = useState("");
|
|
||||||
const [currentPasswordError, setCurrentPasswordError] = useState(false);
|
|
||||||
const [currentPassword, setCurrentPassword] = useState("");
|
|
||||||
const [newPassword, setNewPassword] = useState("");
|
|
||||||
const [backupPassword, setBackupPassword] = useState("");
|
|
||||||
const [passwordChanged, setPasswordChanged] = useState(false);
|
|
||||||
const [backupKeyIssued, setBackupKeyIssued] = useState(false);
|
|
||||||
const [backupKeyError, setBackupKeyError] = useState(false);
|
|
||||||
const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false);
|
|
||||||
const [apiKeys, setApiKeys] = useState<any[]>([]);
|
|
||||||
const [errors, setErrors] = useState<Errors>({});
|
|
||||||
|
|
||||||
const revokeAllSessions = useRevokeAllSessions();
|
|
||||||
|
|
||||||
const { t, i18n } = useTranslation();
|
|
||||||
const router = useRouter();
|
|
||||||
const lang = router.locale ?? "en";
|
|
||||||
|
|
||||||
const setLanguage = async (to: string) => {
|
|
||||||
router.push(router.asPath, router.asPath, { locale: to });
|
|
||||||
localStorage.setItem("lang", to);
|
|
||||||
i18n.changeLanguage(to);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const load = async () => {
|
|
||||||
try {
|
|
||||||
const user = await getUser();
|
|
||||||
setApiKeys(await getAPIKeys());
|
|
||||||
setPersonalEmail(user.email);
|
|
||||||
setPersonalName(`${user.firstName} ${user.lastName}`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
load();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeAddApiKeyModal = () => {
|
|
||||||
setIsAddApiKeyDialogOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex max-h-screen flex-col justify-between bg-bunker-800 text-white">
|
<div className="bg-bunker-800 text-white h-full">
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t("common.head-title", { title: t("settings.personal.title") })}</title>
|
<title>{t("common.head-title", { title: t("settings.personal.title") })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<AddApiKeyDialog
|
<PersonalSettingsPage />
|
||||||
isOpen={isAddApiKeyDialogOpen}
|
|
||||||
closeModal={closeAddApiKeyModal}
|
|
||||||
apiKeys={apiKeys}
|
|
||||||
setApiKeys={setApiKeys}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<div className="max-h-screen w-full pb-2">
|
|
||||||
<NavHeader pageName={t("settings.personal.title")} isProjectRelated={false} />
|
|
||||||
<div className="ml-6 mt-8 mb-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("settings.personal.title")}</p>
|
|
||||||
<p className="mr-4 text-base font-normal text-gray-400">
|
|
||||||
{t("settings.personal.description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="ml-6 mr-6 flex max-w-5xl flex-col text-mineshaft-50">
|
|
||||||
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
|
|
||||||
<p className="self-start text-xl font-semibold">
|
|
||||||
{t("settings.personal.change-language")}
|
|
||||||
</p>
|
|
||||||
<div className="w-ful mt-4 max-h-28">
|
|
||||||
<ListBox
|
|
||||||
isSelected={lang}
|
|
||||||
onChange={setLanguage}
|
|
||||||
data={["en", "ko", "fr", "es"]}
|
|
||||||
text={`${t("common.language")}: `}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SecuritySection />
|
|
||||||
<div className="mt-2 mb-8 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-4">
|
|
||||||
<div className="flex w-full flex-row justify-between">
|
|
||||||
<div className="flex w-full flex-col">
|
|
||||||
<p className="mb-3 text-xl font-semibold">
|
|
||||||
{t("settings.personal.api-keys.title")}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400">
|
|
||||||
{t("settings.personal.api-keys.description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 w-40">
|
|
||||||
<Button
|
|
||||||
text={String(t("settings.personal.api-keys.add-new"))}
|
|
||||||
onButtonPressed={() => {
|
|
||||||
setIsAddApiKeyDialogOpen(true);
|
|
||||||
}}
|
|
||||||
color="mineshaft"
|
|
||||||
icon={faPlus}
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ApiKeyTable data={apiKeys} setApiKeys={setApiKeys as any} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-5 pb-6">
|
|
||||||
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
|
|
||||||
<div className="flex w-full max-w-3xl flex-col justify-between">
|
|
||||||
<p className="mb-3 min-w-max text-xl font-semibold">
|
|
||||||
{t("section.password.change")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full max-w-xl">
|
|
||||||
<InputField
|
|
||||||
label={t("section.password.current") as string}
|
|
||||||
onChangeHandler={(password) => {
|
|
||||||
setCurrentPassword(password);
|
|
||||||
}}
|
|
||||||
type="password"
|
|
||||||
value={currentPassword}
|
|
||||||
isRequired
|
|
||||||
error={currentPasswordError}
|
|
||||||
errorText={t("section.password.current-wrong") as string}
|
|
||||||
autoComplete="current-password"
|
|
||||||
id="current-password"
|
|
||||||
/>
|
|
||||||
<div className="py-2" />
|
|
||||||
<InputField
|
|
||||||
label={t("section.password.new") as string}
|
|
||||||
onChangeHandler={(password) => {
|
|
||||||
setNewPassword(password);
|
|
||||||
checkPassword({
|
|
||||||
password,
|
|
||||||
commonPasswords,
|
|
||||||
setErrors
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
type="password"
|
|
||||||
value={newPassword}
|
|
||||||
isRequired
|
|
||||||
error={Object.keys(errors).length > 0}
|
|
||||||
autoComplete="new-password"
|
|
||||||
id="new-password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{Object.keys(errors).length > 0 && (
|
|
||||||
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
|
|
||||||
<div className="mb-2 text-sm text-gray-400">{t("section.password.validate-base")}</div>
|
|
||||||
{Object.keys(errors).map((key) => {
|
|
||||||
if (errors[key as keyof Errors]) {
|
|
||||||
return (
|
|
||||||
<div className="ml-1 flex flex-row items-top justify-start" key={key}>
|
|
||||||
<div>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faXmark}
|
|
||||||
className="text-md text-red ml-0.5 mr-2.5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-400 text-sm">
|
|
||||||
{errors[key as keyof Errors]}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="mt-3 flex w-52 flex-row items-center pr-3">
|
|
||||||
<Button
|
|
||||||
text={t("section.password.change") as string}
|
|
||||||
onButtonPressed={() => {
|
|
||||||
const errorCheck = checkPassword({
|
|
||||||
password: newPassword,
|
|
||||||
commonPasswords,
|
|
||||||
setErrors
|
|
||||||
});
|
|
||||||
if (!errorCheck) {
|
|
||||||
changePassword(
|
|
||||||
personalEmail,
|
|
||||||
currentPassword,
|
|
||||||
newPassword,
|
|
||||||
setCurrentPasswordError,
|
|
||||||
setPasswordChanged,
|
|
||||||
setCurrentPassword,
|
|
||||||
setNewPassword
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
active={Object.keys(errors).length === 0 && currentPassword !== ""}
|
|
||||||
color="mineshaft"
|
|
||||||
size="md"
|
|
||||||
textDisabled={t("section.password.change") as string}
|
|
||||||
/>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faCheck}
|
|
||||||
className={`ml-4 text-3xl text-primary ${
|
|
||||||
passwordChanged ? "opacity-100" : "opacity-0"
|
|
||||||
} duration-300`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
|
|
||||||
<div className="my-4 flex w-full flex-row justify-between">
|
|
||||||
<p className="text-xl font-semibold w-full">
|
|
||||||
Sessions
|
|
||||||
</p>
|
|
||||||
<div className="w-40">
|
|
||||||
<Button
|
|
||||||
text="Revoke all"
|
|
||||||
onButtonPressed={async () => {
|
|
||||||
await revokeAllSessions.mutateAsync();
|
|
||||||
router.push("/login");
|
|
||||||
}}
|
|
||||||
color="mineshaft"
|
|
||||||
icon={faBan}
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="mb-5 text-sm text-mineshaft-300">
|
|
||||||
Logging into Infisical via browser or CLI creates a session. Revoking all sessions logs your account out all active sessions across all browsers and CLIs.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-2 mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-5 pb-6">
|
|
||||||
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
|
|
||||||
<div className="flex w-full max-w-3xl flex-col justify-between">
|
|
||||||
<p className="mb-3 min-w-max text-xl font-semibold">
|
|
||||||
{t("settings.personal.emergency.name")}
|
|
||||||
</p>
|
|
||||||
<p className="min-w-max text-sm text-mineshaft-300">
|
|
||||||
{t("settings.personal.emergency.text1")}
|
|
||||||
</p>
|
|
||||||
<p className="mb-5 min-w-max text-sm text-mineshaft-300">
|
|
||||||
{t("settings.personal.emergency.text2")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4 w-full max-w-xl">
|
|
||||||
<InputField
|
|
||||||
label={t("section.password.current") as string}
|
|
||||||
onChangeHandler={setBackupPassword}
|
|
||||||
type="password"
|
|
||||||
value={backupPassword}
|
|
||||||
isRequired
|
|
||||||
error={backupKeyError}
|
|
||||||
errorText={t("section.password.current-wrong") as string}
|
|
||||||
autoComplete="current-password"
|
|
||||||
id="current-password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 flex w-60 flex-row items-center">
|
|
||||||
<Button
|
|
||||||
text={t("settings.personal.emergency.download") as string}
|
|
||||||
onButtonPressed={() => {
|
|
||||||
issueBackupKey({
|
|
||||||
email: personalEmail,
|
|
||||||
password: backupPassword,
|
|
||||||
personalName,
|
|
||||||
setBackupKeyError,
|
|
||||||
setBackupKeyIssued
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
color="mineshaft"
|
|
||||||
size="md"
|
|
||||||
active={backupPassword !== ""}
|
|
||||||
textDisabled={t("settings.personal.emergency.download") as string}
|
|
||||||
/>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faCheck}
|
|
||||||
className={`ml-4 text-3xl text-primary ${
|
|
||||||
backupKeyIssued ? "opacity-100" : "opacity-0"
|
|
||||||
} duration-300`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,9 @@ export default function Users() {
|
|||||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<NavHeader pageName={t("settings.members.title")} isProjectRelated />
|
<div className="ml-6">
|
||||||
|
<NavHeader pageName={t("settings.members.title")} isProjectRelated />
|
||||||
|
</div>
|
||||||
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
|
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
|
||||||
<p className="mr-4 font-semibold text-white">{t("settings.members.title")}</p>
|
<p className="mr-4 font-semibold text-white">{t("settings.members.title")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export const DashboardEnvOverview = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
<div className="relative right-5">
|
<div className="relative right-5 ml-4">
|
||||||
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
|
|||||||
@@ -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("");
|
||||||
@@ -542,7 +545,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
<FormProvider {...method}>
|
<FormProvider {...method}>
|
||||||
<form autoComplete="off" className="h-full">
|
<form autoComplete="off" className="h-full">
|
||||||
{/* breadcrumb row */}
|
{/* breadcrumb row */}
|
||||||
<div className="relative right-6 -top-2 mb-2">
|
<div className="relative right-6 -top-2 mb-2 ml-6">
|
||||||
<NavHeader
|
<NavHeader
|
||||||
pageName={t("dashboard.title")}
|
pageName={t("dashboard.title")}
|
||||||
currentEnv={
|
currentEnv={
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
/* eslint-disable react/jsx-no-useless-fragment */
|
/* eslint-disable react/jsx-no-useless-fragment */
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { faCircle, faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-icons";
|
import { faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
import { useSyntaxHighlight } from "@app/hooks";
|
||||||
|
|
||||||
|
import { useToggle } from "~/hooks/useToggle";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
secrets: any[] | undefined;
|
secrets: any[] | undefined;
|
||||||
@@ -12,7 +15,8 @@ type Props = {
|
|||||||
userAvailableEnvs?: any[];
|
userAvailableEnvs?: any[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const REGEX = /([$]{.*?})/g;
|
const SEC_VAL_LINE_HEIGHT = 21;
|
||||||
|
const MAX_MULTI_LINE = 6;
|
||||||
|
|
||||||
const DashboardInput = ({
|
const DashboardInput = ({
|
||||||
isOverridden,
|
isOverridden,
|
||||||
@@ -25,84 +29,60 @@ const DashboardInput = ({
|
|||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
secret?: any;
|
secret?: any;
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const syntaxHighlight = useCallback((val: string) => {
|
const ref = useRef<HTMLElement | null>(null);
|
||||||
if (val === undefined)
|
const [isFocused, setIsFocused] = useToggle();
|
||||||
return (
|
const syntaxHighlight = useSyntaxHighlight();
|
||||||
<span className="cursor-default font-sans text-xs italic text-red-500/80">
|
|
||||||
<FontAwesomeIcon icon={faMinus} className="mt-1" />
|
const value = isOverridden ? secret.valueOverride : secret?.value;
|
||||||
</span>
|
const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT;
|
||||||
);
|
const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE);
|
||||||
if (val?.length === 0)
|
|
||||||
return <span className="w-full font-sans text-bunker-400/80">EMPTY</span>;
|
|
||||||
return val?.split(REGEX).map((word, index) =>
|
|
||||||
word.match(REGEX) !== null ? (
|
|
||||||
<span className="ph-no-capture text-yellow" key={`${val}-${index + 1}`}>
|
|
||||||
{word.slice(0, 2)}
|
|
||||||
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
|
|
||||||
{word.slice(word.length - 1, word.length) === "}" ? (
|
|
||||||
<span className="ph-no-capture text-yellow">
|
|
||||||
{word.slice(word.length - 1, word.length)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="ph-no-capture text-yellow-400">
|
|
||||||
{word.slice(word.length - 1, word.length)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span key={word} className="ph-no-capture">
|
|
||||||
{word}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key={`row-${secret?.key || ""}--`}
|
key={`row-${secret?.key || ""}--`}
|
||||||
className={`flex h-10 w-full cursor-default flex-row items-center justify-center ${
|
className={`flex w-full cursor-default flex-row ${
|
||||||
!(secret?.value || secret?.value === "") ? "bg-red-800/10" : "bg-mineshaft-900/30"
|
!(secret?.value || secret?.value === "") ? "bg-red-800/10" : "bg-mineshaft-900/30"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="group relative flex w-full cursor-default flex-col justify-center whitespace-pre">
|
<div className="group relative flex w-full flex-col whitespace-pre px-1.5 pt-1.5">
|
||||||
<input
|
<textarea
|
||||||
value={isOverridden ? secret.valueOverride : secret?.value || ""}
|
|
||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
className={twMerge(
|
value={value}
|
||||||
"ph-no-capture no-scrollbar::-webkit-scrollbar duration-50 peer z-10 w-full cursor-default bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-transparent outline-none no-scrollbar",
|
className="ph-no-capture min-w-16 duration-50 peer z-20 w-full resize-none overflow-auto text-ellipsis bg-transparent px-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar"
|
||||||
isSecretValueHidden && "text-transparent focus:text-transparent active:text-transparent"
|
style={{ height: `${maxMultilineHeight}px` }}
|
||||||
)}
|
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
|
onBlur={() => setIsFocused.off()}
|
||||||
|
onFocus={() => setIsFocused.on()}
|
||||||
|
onInput={(el) => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.scrollTop = el.currentTarget.scrollTop;
|
||||||
|
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onScroll={(el) => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.scrollTop = el.currentTarget.scrollTop;
|
||||||
|
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<pre className="whitespace-pre-wrap break-words">
|
||||||
className={twMerge(
|
<code
|
||||||
"ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 absolute z-0 mt-0.5 flex h-10 w-full cursor-default flex-row overflow-x-scroll whitespace-pre bg-transparent px-2 py-2 font-mono text-sm outline-none no-scrollbar peer-focus:visible",
|
ref={ref}
|
||||||
isSecretValueHidden && secret?.value ? "invisible" : "visible",
|
className={`absolute top-1.5 left-3.5 z-10 overflow-auto font-mono text-sm transition-all no-scrollbar ${
|
||||||
isSecretValueHidden &&
|
isOverridden && "text-primary-300"
|
||||||
secret?.value &&
|
}`}
|
||||||
"duration-50 text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400",
|
style={{ height: `${maxMultilineHeight}px`, width: "calc(100% - 12px)" }}
|
||||||
!secret?.value && "justify-center text-bunker-400"
|
>
|
||||||
)}
|
{value === undefined ? (
|
||||||
>
|
<span className="cursor-default font-sans text-xs italic text-red-500/80">
|
||||||
{syntaxHighlight(secret?.value)}
|
<FontAwesomeIcon icon={faMinus} className="mt-1" />
|
||||||
</div>
|
</span>
|
||||||
{isSecretValueHidden && secret?.value && (
|
) : (
|
||||||
<div className="duration-50 peer absolute z-0 flex h-10 w-full flex-row items-center justify-between text-clip pr-2 text-bunker-400 group-hover:bg-white/[0.00] peer-focus:hidden peer-active:hidden">
|
syntaxHighlight(value || "", isSecretValueHidden ? !isFocused : isSecretValueHidden)
|
||||||
<div className="no-scrollbar::-webkit-scrollbar flex flex-row items-center overflow-x-scroll px-2 no-scrollbar">
|
)}
|
||||||
{(isOverridden ? secret.valueOverride : secret?.value || "")
|
</code>
|
||||||
?.split("")
|
</pre>
|
||||||
.map((_a: string, index: number) => (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
key={`${secret?.value}_${index + 1}`}
|
|
||||||
className="mr-0.5 text-xxs"
|
|
||||||
icon={faCircle}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{(isOverridden ? secret.valueOverride : secret?.value || "")?.split("").length ===
|
|
||||||
0 && <span className="text-sm text-bunker-400/80">EMPTY</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
@@ -122,14 +102,14 @@ export const EnvComparisonRow = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800">
|
<tr className="group flex min-w-full flex-row hover:bg-mineshaft-800">
|
||||||
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
<td className="flex w-10 justify-center border-none px-4">
|
||||||
<div className="w-10 text-center text-xs text-bunker-400">
|
<div className="flex h-8 w-10 items-center justify-center text-center text-xs text-bunker-400">
|
||||||
<FontAwesomeIcon icon={faKey} />
|
<FontAwesomeIcon icon={faKey} />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
<td className="flex min-w-[200px] flex-row justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
||||||
<div className="flex h-8 cursor-default flex-row items-center truncate">
|
<div className="flex h-8 cursor-default flex-row items-center justify-center truncate">
|
||||||
{secrets![0].key || ""}
|
{secrets![0].key || ""}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
+2
-2
@@ -193,8 +193,8 @@ export const SecretDetailDrawer = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
|
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
|
||||||
<div className="rounded-sm bg-primary-500/30 px-1">Value:</div>
|
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
|
||||||
<div className="font-mono">{value}</div>
|
<div className="break-all font-mono">{value}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useCallback } from "react";
|
import { useRef } from "react";
|
||||||
import { useFormContext, useWatch } from "react-hook-form";
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
import { faCircle } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { useSyntaxHighlight, useToggle } from "@app/hooks";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { FormData } from "../../DashboardPage.utils";
|
import { FormData } from "../../DashboardPage.utils";
|
||||||
|
|
||||||
@@ -13,95 +12,94 @@ type Props = {
|
|||||||
index: number;
|
index: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const REGEX = /([$]{.*?})/g;
|
const SEC_VAL_LINE_HEIGHT = 21;
|
||||||
|
const MAX_MULTI_LINE = 6;
|
||||||
|
|
||||||
export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridden }: Props) => {
|
export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridden }: Props) => {
|
||||||
const { register, control } = useFormContext<FormData>();
|
const { control } = useFormContext<FormData>();
|
||||||
|
const ref = useRef<HTMLElement | null>(null);
|
||||||
|
const [isFocused, setIsFocused] = useToggle();
|
||||||
|
const syntaxHighlight = useSyntaxHighlight();
|
||||||
|
|
||||||
const secretValue = useWatch({ control, name: `secrets.${index}.value` });
|
const secretValue = useWatch({ control, name: `secrets.${index}.value` });
|
||||||
const secretValueOverride = useWatch({ control, name: `secrets.${index}.valueOverride` });
|
const secretValueOverride = useWatch({ control, name: `secrets.${index}.valueOverride` });
|
||||||
const value = isOverridden ? secretValueOverride : secretValue;
|
const value = isOverridden ? secretValueOverride : secretValue;
|
||||||
|
|
||||||
const syntaxHighlight = useCallback((val: string) => {
|
const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT;
|
||||||
if (val?.length === 0) return <span className="font-sans text-bunker-400/80">EMPTY</span>;
|
const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE);
|
||||||
return val?.split(REGEX).map((word, i) =>
|
|
||||||
word.match(REGEX) !== null ? (
|
|
||||||
<span className="ph-no-capture text-yellow" key={`${val}-${i + 1}`}>
|
|
||||||
{word.slice(0, 2)}
|
|
||||||
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
|
|
||||||
{word.slice(word.length - 1, word.length) === "}" ? (
|
|
||||||
<span className="ph-no-capture text-yellow">
|
|
||||||
{word.slice(word.length - 1, word.length)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="ph-no-capture text-yellow-400">
|
|
||||||
{word.slice(word.length - 1, word.length)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span key={`${word}_${i + 1}`} className="ph-no-capture">
|
|
||||||
{word}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative flex w-full flex-col justify-center whitespace-pre px-1.5">
|
<div className="group relative flex w-full flex-col whitespace-pre px-1.5 pt-1.5">
|
||||||
{isOverridden ? (
|
{isOverridden ? (
|
||||||
<input
|
<Controller
|
||||||
{...register(`secrets.${index}.valueOverride`)}
|
control={control}
|
||||||
readOnly={isReadOnly}
|
name={`secrets.${index}.valueOverride`}
|
||||||
className={twMerge(
|
render={({ field }) => (
|
||||||
"ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 peer z-10 w-full bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar",
|
<textarea
|
||||||
!isSecretValueHidden &&
|
key={`secrets.${index}.valueOverride`}
|
||||||
"text-transparent focus:text-transparent active:text-transparent"
|
{...field}
|
||||||
|
readOnly={isReadOnly}
|
||||||
|
className="ph-no-capture min-w-16 duration-50 peer z-20 w-full resize-none overflow-auto text-ellipsis bg-transparent px-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar"
|
||||||
|
style={{ height: `${maxMultilineHeight}px` }}
|
||||||
|
spellCheck="false"
|
||||||
|
onBlur={() => setIsFocused.off()}
|
||||||
|
onFocus={() => setIsFocused.on()}
|
||||||
|
onInput={(el) => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.scrollTop = el.currentTarget.scrollTop;
|
||||||
|
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onScroll={(el) => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.scrollTop = el.currentTarget.scrollTop;
|
||||||
|
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
spellCheck="false"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<Controller
|
||||||
{...register(`secrets.${index}.value`)}
|
control={control}
|
||||||
readOnly={isReadOnly}
|
name={`secrets.${index}.value`}
|
||||||
className={twMerge(
|
key={`secrets.${index}.value`}
|
||||||
"ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 peer z-10 w-full bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar",
|
render={({ field }) => (
|
||||||
!isSecretValueHidden &&
|
<textarea
|
||||||
"text-transparent focus:text-transparent active:text-transparent"
|
{...field}
|
||||||
|
readOnly={isReadOnly}
|
||||||
|
className="ph-no-capture min-w-16 duration-50 peer z-20 w-full resize-none overflow-auto text-ellipsis bg-transparent px-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar"
|
||||||
|
style={{ height: `${maxMultilineHeight}px` }}
|
||||||
|
spellCheck="false"
|
||||||
|
onBlur={() => setIsFocused.off()}
|
||||||
|
onFocus={() => setIsFocused.on()}
|
||||||
|
onInput={(el) => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.scrollTop = el.currentTarget.scrollTop;
|
||||||
|
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onScroll={(el) => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.scrollTop = el.currentTarget.scrollTop;
|
||||||
|
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
spellCheck="false"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<pre className="whitespace-pre-wrap break-words">
|
||||||
className={twMerge(
|
<code
|
||||||
"ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 absolute z-0 mt-0.5 flex h-10 w-full flex-row overflow-x-scroll whitespace-pre bg-transparent px-2 py-2 font-mono text-sm outline-none no-scrollbar peer-focus:visible",
|
ref={ref}
|
||||||
isSecretValueHidden ? "invisible" : "visible",
|
className={`absolute top-1.5 left-3.5 z-10 w-full overflow-auto font-mono text-sm transition-all no-scrollbar ${
|
||||||
isOverridden
|
isOverridden && "text-primary-300"
|
||||||
? "text-primary-300"
|
}`}
|
||||||
: "duration-50 text-gray-400 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400"
|
style={{ height: `${maxMultilineHeight}px`, width: "calc(100% - 12px)" }}
|
||||||
)}
|
>
|
||||||
>
|
{syntaxHighlight(value || "", isSecretValueHidden ? !isFocused : isSecretValueHidden)}
|
||||||
{syntaxHighlight(value || "")}
|
</code>
|
||||||
</div>
|
</pre>
|
||||||
<div
|
|
||||||
className={twMerge(
|
|
||||||
"duration-50 peer absolute z-0 flex h-10 w-full flex-row items-center justify-between text-clip pr-2 text-bunker-400 group-hover:bg-white/[0.00] peer-focus:hidden peer-active:hidden",
|
|
||||||
!isSecretValueHidden ? "invisible" : "visible"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="no-scrollbar::-webkit-scrollbar flex flex-row items-center overflow-x-scroll px-2 no-scrollbar">
|
|
||||||
{value?.split("").map((val, i) => (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
key={`${value}_${val}_${i + 1}`}
|
|
||||||
className="mr-0.5 text-xxs"
|
|
||||||
icon={faCircle}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{value?.split("").length === 0 && (
|
|
||||||
<span className="text-sm text-bunker-400/80">EMPTY</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export const SecretInputRow = memo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="group flex flex-row items-center" key={index}>
|
<tr className="group flex flex-row" key={index}>
|
||||||
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
||||||
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
|
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -198,7 +198,7 @@ export const SecretInputRow = memo(
|
|||||||
</HoverCard>
|
</HoverCard>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<td className="flex h-10 w-full flex-grow flex-row items-center justify-center border-r border-none border-red">
|
<td className="flex w-full flex-grow flex-row border-r border-none border-red">
|
||||||
<MaskedInput
|
<MaskedInput
|
||||||
isReadOnly={
|
isReadOnly={
|
||||||
isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
||||||
@@ -208,8 +208,8 @@ export const SecretInputRow = memo(
|
|||||||
index={index}
|
index={index}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="min-w-sm flex h-10 items-center">
|
<td className="min-w-sm flex">
|
||||||
<div className="flex items-center pl-2">
|
<div className="flex h-8 items-center pl-2">
|
||||||
{secretTags.map(({ id, slug }, i) => (
|
{secretTags.map(({ id, slug }, i) => (
|
||||||
<Tag
|
<Tag
|
||||||
className={cx(
|
className={cx(
|
||||||
@@ -289,7 +289,7 @@ export const SecretInputRow = memo(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full flex-row items-center pr-2">
|
<div className="flex h-8 flex-row items-center pr-2">
|
||||||
{!isAddOnly && (
|
{!isAddOnly && (
|
||||||
<div>
|
<div>
|
||||||
<Tooltip content="Override with a personal value">
|
<Tooltip content="Override with a personal value">
|
||||||
@@ -346,35 +346,37 @@ export const SecretInputRow = memo(
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
<div className="duration-0 flex w-16 justify-center overflow-hidden border-l border-mineshaft-600 pl-2 transition-all">
|
||||||
{!isAddOnly && (
|
<div className="flex h-8 items-center space-x-2.5">
|
||||||
|
{!isAddOnly && (
|
||||||
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
<Tooltip content="Settings">
|
||||||
|
<IconButton
|
||||||
|
size="lg"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={onRowExpand}
|
||||||
|
ariaLabel="expand"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEllipsis} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
<Tooltip content="Settings">
|
<Tooltip content="Delete">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="lg"
|
size="md"
|
||||||
colorSchema="primary"
|
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={onRowExpand}
|
colorSchema="danger"
|
||||||
ariaLabel="expand"
|
ariaLabel="delete"
|
||||||
|
isDisabled={isReadOnly || isRollbackMode}
|
||||||
|
onClick={() => onSecretDelete(index, secId, idOverride)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faEllipsis} />
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
|
||||||
<Tooltip content="Delete">
|
|
||||||
<IconButton
|
|
||||||
size="md"
|
|
||||||
variant="plain"
|
|
||||||
colorSchema="danger"
|
|
||||||
ariaLabel="delete"
|
|
||||||
isDisabled={isReadOnly || isRollbackMode}
|
|
||||||
onClick={() => onSecretDelete(index, secId, idOverride)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -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,224 @@
|
|||||||
|
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">
|
||||||
|
<div className="ml-4">
|
||||||
|
<NavHeader pageName={t("integrations.title")} isProjectRelated />
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+140
@@ -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";
|
||||||
+54
@@ -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";
|
||||||
+149
@@ -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,23 @@
|
|||||||
|
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="flex justify-center bg-bunker-800 text-white w-full h-full px-6">
|
||||||
|
<div className="max-w-screen-lg w-full">
|
||||||
|
<NavHeader pageName={t("billing.title")} />
|
||||||
|
<div className="my-8">
|
||||||
|
<p className="text-3xl font-semibold text-gray-200">{t("billing.title")}</p>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
<BillingTabGroup />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { CurrentPlanSection } from "./CurrentPlanSection";
|
||||||
|
import { PreviewSection } from "./PreviewSection";
|
||||||
|
|
||||||
|
export const BillingCloudTab = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PreviewSection />
|
||||||
|
<CurrentPlanSection />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+87
@@ -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 mb-6 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+64
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+176
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+100
@@ -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 mb-6 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 && subscription && data && (
|
||||||
|
<div className="flex mb-6 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 →
|
||||||
|
</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";
|
||||||
+15
@@ -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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
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} 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 { mutateAsync, isLoading } = useUpdateOrgBillingDetails();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
reset({
|
||||||
|
name: data?.name ?? ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ name }: { name: string }) => {
|
||||||
|
try {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
if (name === "") return;
|
||||||
|
await 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 mb-6 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>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
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} 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 { mutateAsync, isLoading } = useUpdateOrgBillingDetails();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
reset({
|
||||||
|
email: data?.email ?? ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ email }: { email: string }) => {
|
||||||
|
try {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
if (email === "") return;
|
||||||
|
|
||||||
|
await 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 mb-6 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>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import { useAddOrgPmtMethod } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { PmtMethodsTable } from "./PmtMethodsTable";
|
||||||
|
|
||||||
|
export const PmtMethodsSection = () => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { mutateAsync, isLoading } = useAddOrgPmtMethod();
|
||||||
|
|
||||||
|
const handleAddPmtMethodBtnClick = async () => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
const url = await 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 mb-6 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>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddPmtMethodBtnClick}
|
||||||
|
colorSchema="secondary"
|
||||||
|
isLoading={isLoading}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add method
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<PmtMethodsTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+90
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+195
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Button } from "@app/components/v2";
|
||||||
|
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 mb-6 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>
|
||||||
|
<Button
|
||||||
|
onClick={() => handlePopUpOpen("addTaxID")}
|
||||||
|
colorSchema="secondary"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add method
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<TaxIDTable />
|
||||||
|
<TaxIDModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpClose={handlePopUpClose}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+137
@@ -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";
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import { InvoicesTable } from "./InvoicesTable";
|
||||||
|
|
||||||
|
export const BillingReceiptsTab = () => {
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||||
|
<h2 className="text-xl font-semibold flex-1 text-white">Invoices</h2>
|
||||||
|
<InvoicesTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+88
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { BillingReceiptsTab } from "./BillingReceiptsTab";
|
||||||
+44
@@ -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 mb-6 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";
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user