mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Fix merge conflicts
This commit is contained in:
@@ -35,14 +35,11 @@ export const getIntegrationAuth = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getIntegrationOptions = async (
|
export const getIntegrationOptions = async (req: Request, res: Response) => {
|
||||||
req: Request,
|
return res.status(200).send({
|
||||||
res: Response
|
integrationOptions: INTEGRATION_OPTIONS,
|
||||||
) => {
|
});
|
||||||
return res.status(200).send({
|
};
|
||||||
integrationOptions: INTEGRATION_OPTIONS
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
||||||
@@ -90,8 +87,8 @@ export const oAuthExchange = async (
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const saveIntegrationAccessToken = async (
|
export const saveIntegrationAccessToken = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
// TODO: refactor
|
// TODO: refactor
|
||||||
// TODO: check if access token is valid for each integration
|
// TODO: check if access token is valid for each integration
|
||||||
@@ -157,23 +154,23 @@ export const saveIntegrationAccessToken = async (
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||||
let apps;
|
let apps;
|
||||||
try {
|
try {
|
||||||
apps = await getApps({
|
apps = await getApps({
|
||||||
integrationAuth: req.integrationAuth,
|
integrationAuth: req.integrationAuth,
|
||||||
accessToken: req.accessToken
|
accessToken: req.accessToken,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get integration authorization applications'
|
message: "Failed to get integration authorization applications",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
apps
|
apps,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,21 +180,21 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
||||||
let integrationAuth;
|
let integrationAuth;
|
||||||
try {
|
try {
|
||||||
integrationAuth = await revokeAccess({
|
integrationAuth = await revokeAccess({
|
||||||
integrationAuth: req.integrationAuth,
|
integrationAuth: req.integrationAuth,
|
||||||
accessToken: req.accessToken
|
accessToken: req.accessToken,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to delete integration authorization'
|
message: "Failed to delete integration authorization",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integrationAuth
|
integrationAuth,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import { eventPushSecrets } from '../../events';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create/initialize an (empty) integration for integration authorization
|
* Create/initialize an (empty) integration for integration authorization
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const createIntegration = async (req: Request, res: Response) => {
|
export const createIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
let integration;
|
||||||
@@ -65,10 +65,10 @@ export const createIntegration = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integration
|
integration,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change environment or name of integration with id [integrationId]
|
* Change environment or name of integration with id [integrationId]
|
||||||
@@ -77,57 +77,57 @@ export const createIntegration = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const updateIntegration = async (req: Request, res: Response) => {
|
export const updateIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
let integration;
|
||||||
|
|
||||||
// TODO: add integration-specific validation to ensure that each
|
|
||||||
// integration has the correct fields populated in [Integration]
|
|
||||||
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
environment,
|
|
||||||
isActive,
|
|
||||||
app,
|
|
||||||
appId,
|
|
||||||
targetEnvironment,
|
|
||||||
owner, // github-specific integration param
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
integration = await Integration.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
_id: req.integration._id
|
|
||||||
},
|
|
||||||
{
|
|
||||||
environment,
|
|
||||||
isActive,
|
|
||||||
app,
|
|
||||||
appId,
|
|
||||||
targetEnvironment,
|
|
||||||
owner
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (integration) {
|
|
||||||
// trigger event - push secrets
|
|
||||||
EventService.handleEvent({
|
|
||||||
event: eventPushSecrets({
|
|
||||||
workspaceId: integration.workspace.toString()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to update integration'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
// TODO: add integration-specific validation to ensure that each
|
||||||
integration
|
// integration has the correct fields populated in [Integration]
|
||||||
});
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
environment,
|
||||||
|
isActive,
|
||||||
|
app,
|
||||||
|
appId,
|
||||||
|
targetEnvironment,
|
||||||
|
owner, // github-specific integration param
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
integration = await Integration.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
_id: req.integration._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
environment,
|
||||||
|
isActive,
|
||||||
|
app,
|
||||||
|
appId,
|
||||||
|
targetEnvironment,
|
||||||
|
owner,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (integration) {
|
||||||
|
// trigger event - push secrets
|
||||||
|
EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: integration.workspace.toString(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "Failed to update integration",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
integration,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,24 +138,24 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteIntegration = async (req: Request, res: Response) => {
|
export const deleteIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
let integration;
|
||||||
try {
|
try {
|
||||||
const { integrationId } = req.params;
|
const { integrationId } = req.params;
|
||||||
|
|
||||||
integration = await Integration.findOneAndDelete({
|
integration = await Integration.findOneAndDelete({
|
||||||
_id: integrationId
|
_id: integrationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!integration) throw new Error('Failed to find integration');
|
if (!integration) throw new Error("Failed to find integration");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to delete integration'
|
message: "Failed to delete integration",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integration
|
integration,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from "express";
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from "@sentry/node";
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
Membership,
|
Membership,
|
||||||
MembershipOrg,
|
MembershipOrg,
|
||||||
Integration,
|
Integration,
|
||||||
IntegrationAuth,
|
IntegrationAuth,
|
||||||
IUser,
|
IUser,
|
||||||
ServiceToken,
|
ServiceToken,
|
||||||
ServiceTokenData
|
ServiceTokenData,
|
||||||
} from '../../models';
|
} from "../../models";
|
||||||
import {
|
import {
|
||||||
createWorkspace as create,
|
createWorkspace as create,
|
||||||
deleteWorkspace as deleteWork
|
deleteWorkspace as deleteWork,
|
||||||
} from '../../helpers/workspace';
|
} from "../../helpers/workspace";
|
||||||
import { addMemberships } from '../../helpers/membership';
|
import { addMemberships } from "../../helpers/membership";
|
||||||
import { ADMIN } from '../../variables';
|
import { ADMIN } from "../../variables";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return public keys of members of workspace with id [workspaceId]
|
* Return public keys of members of workspace with id [workspaceId]
|
||||||
@@ -24,32 +24,31 @@ import { ADMIN } from '../../variables';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
||||||
let publicKeys;
|
let publicKeys;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
publicKeys = (
|
publicKeys = (
|
||||||
await Membership.find({
|
await Membership.find({
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
}).populate<{ user: IUser }>('user', 'publicKey')
|
}).populate<{ user: IUser }>("user", "publicKey")
|
||||||
)
|
).map((member) => {
|
||||||
.map((member) => {
|
return {
|
||||||
return {
|
publicKey: member.user.publicKey,
|
||||||
publicKey: member.user.publicKey,
|
userId: member.user._id,
|
||||||
userId: member.user._id
|
};
|
||||||
};
|
});
|
||||||
});
|
} catch (err) {
|
||||||
} catch (err) {
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.captureException(err);
|
||||||
Sentry.captureException(err);
|
return res.status(400).send({
|
||||||
return res.status(400).send({
|
message: "Failed to get workspace member public keys",
|
||||||
message: 'Failed to get workspace member public keys'
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
publicKeys
|
publicKeys,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,24 +58,24 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||||
let users;
|
let users;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
users = await Membership.find({
|
users = await Membership.find({
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
}).populate('user', '+publicKey');
|
}).populate("user", "+publicKey");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get workspace members'
|
message: "Failed to get workspace members",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
users
|
users,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,24 +85,24 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaces = async (req: Request, res: Response) => {
|
export const getWorkspaces = async (req: Request, res: Response) => {
|
||||||
let workspaces;
|
let workspaces;
|
||||||
try {
|
try {
|
||||||
workspaces = (
|
workspaces = (
|
||||||
await Membership.find({
|
await Membership.find({
|
||||||
user: req.user._id
|
user: req.user._id,
|
||||||
}).populate('workspace')
|
}).populate("workspace")
|
||||||
).map((m) => m.workspace);
|
).map((m) => m.workspace);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get workspaces'
|
message: "Failed to get workspaces",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
workspaces
|
workspaces,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,24 +112,24 @@ export const getWorkspaces = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspace = async (req: Request, res: Response) => {
|
export const getWorkspace = async (req: Request, res: Response) => {
|
||||||
let workspace;
|
let workspace;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
workspace = await Workspace.findOne({
|
workspace = await Workspace.findOne({
|
||||||
_id: workspaceId
|
_id: workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get workspace'
|
message: "Failed to get workspace",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
workspace
|
workspace,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,46 +140,46 @@ export const getWorkspace = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const createWorkspace = async (req: Request, res: Response) => {
|
export const createWorkspace = async (req: Request, res: Response) => {
|
||||||
let workspace;
|
let workspace;
|
||||||
try {
|
try {
|
||||||
const { workspaceName, organizationId } = req.body;
|
const { workspaceName, organizationId } = req.body;
|
||||||
|
|
||||||
// validate organization membership
|
// validate organization membership
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
organization: organizationId
|
organization: organizationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipOrg) {
|
if (!membershipOrg) {
|
||||||
throw new Error('Failed to validate organization membership');
|
throw new Error("Failed to validate organization membership");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workspaceName.length < 1) {
|
if (workspaceName.length < 1) {
|
||||||
throw new Error('Workspace names must be at least 1-character long');
|
throw new Error("Workspace names must be at least 1-character long");
|
||||||
}
|
}
|
||||||
|
|
||||||
// create workspace and add user as member
|
// create workspace and add user as member
|
||||||
workspace = await create({
|
workspace = await create({
|
||||||
name: workspaceName,
|
name: workspaceName,
|
||||||
organizationId
|
organizationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await addMemberships({
|
await addMemberships({
|
||||||
userIds: [req.user._id],
|
userIds: [req.user._id],
|
||||||
workspaceId: workspace._id.toString(),
|
workspaceId: workspace._id.toString(),
|
||||||
roles: [ADMIN]
|
roles: [ADMIN],
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to create workspace'
|
message: "Failed to create workspace",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
workspace
|
workspace,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,24 +189,24 @@ export const createWorkspace = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteWorkspace = async (req: Request, res: Response) => {
|
export const deleteWorkspace = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
// delete workspace
|
// delete workspace
|
||||||
await deleteWork({
|
await deleteWork({
|
||||||
id: workspaceId
|
id: workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to delete workspace'
|
message: "Failed to delete workspace",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully deleted workspace'
|
message: "Successfully deleted workspace",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -217,34 +216,34 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const changeWorkspaceName = async (req: Request, res: Response) => {
|
export const changeWorkspaceName = async (req: Request, res: Response) => {
|
||||||
let workspace;
|
let workspace;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const { name } = req.body;
|
const { name } = req.body;
|
||||||
|
|
||||||
workspace = await Workspace.findOneAndUpdate(
|
workspace = await Workspace.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
_id: workspaceId
|
_id: workspaceId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name
|
name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to change workspace name'
|
message: "Failed to change workspace name",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully changed workspace name',
|
message: "Successfully changed workspace name",
|
||||||
workspace
|
workspace,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -254,24 +253,24 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
||||||
let integrations;
|
let integrations;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
integrations = await Integration.find({
|
integrations = await Integration.find({
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get workspace integrations'
|
message: "Failed to get workspace integrations",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integrations
|
integrations,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -281,56 +280,56 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceIntegrationAuthorizations = async (
|
export const getWorkspaceIntegrationAuthorizations = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let authorizations;
|
let authorizations;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
authorizations = await IntegrationAuth.find({
|
authorizations = await IntegrationAuth.find({
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get workspace integration authorizations'
|
message: "Failed to get workspace integration authorizations",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
authorizations
|
authorizations,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return service service tokens for workspace [workspaceId] belonging to user
|
* Return service service tokens for workspace [workspaceId] belonging to user
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceServiceTokens = async (
|
export const getWorkspaceServiceTokens = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let serviceTokens;
|
let serviceTokens;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
// ?? FIX.
|
// ?? FIX.
|
||||||
serviceTokens = await ServiceToken.find({
|
serviceTokens = await ServiceToken.find({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get workspace service tokens'
|
message: "Failed to get workspace service tokens",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serviceTokens
|
serviceTokens,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { postHogClient } from '../../services';
|
|||||||
import { getChannelFromUserAgent } from '../../utils/posthog';
|
import { getChannelFromUserAgent } from '../../utils/posthog';
|
||||||
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
|
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
|
||||||
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
|
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
|
||||||
|
import Tag from '../../models/tag';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create secret(s) for workspace with id [workspaceId] and environment [environment]
|
* Create secret(s) for workspace with id [workspaceId] and environment [environment]
|
||||||
@@ -284,8 +286,10 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
const { workspaceId, environment } = req.query;
|
|
||||||
|
|
||||||
|
|
||||||
|
const { workspaceId, environment, tagSlugs } = req.query;
|
||||||
|
const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : [];
|
||||||
let userId = "" // used for getting personal secrets for user
|
let userId = "" // used for getting personal secrets for user
|
||||||
let userEmail = "" // used for posthog
|
let userEmail = "" // used for posthog
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
@@ -308,31 +312,42 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let secrets: any
|
let secrets: any
|
||||||
if (hasWriteOnlyAccess) {
|
let secretQuery: any
|
||||||
secrets = await Secret.find(
|
|
||||||
{
|
if (tagNamesList != undefined && tagNamesList.length != 0) {
|
||||||
workspace: workspaceId,
|
const workspaceFromDB = await Tag.find({ workspace: workspaceId })
|
||||||
environment,
|
|
||||||
$or: [
|
const tagIds = _.map(tagNamesList, (tagName) => {
|
||||||
{ user: userId },
|
const tag = _.find(workspaceFromDB, { slug: tagName });
|
||||||
{ user: { $exists: false } }
|
return tag ? tag.id : null;
|
||||||
],
|
});
|
||||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
|
||||||
}
|
secretQuery = {
|
||||||
)
|
workspace: workspaceId,
|
||||||
.select("secretKeyCiphertext secretKeyIV secretKeyTag")
|
environment,
|
||||||
|
$or: [
|
||||||
|
{ user: userId },
|
||||||
|
{ user: { $exists: false } }
|
||||||
|
],
|
||||||
|
tags: { $in: tagIds },
|
||||||
|
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
secrets = await Secret.find(
|
secretQuery = {
|
||||||
{
|
workspace: workspaceId,
|
||||||
workspace: workspaceId,
|
environment,
|
||||||
environment,
|
$or: [
|
||||||
$or: [
|
{ user: userId },
|
||||||
{ user: userId },
|
{ user: { $exists: false } }
|
||||||
{ user: { $exists: false } }
|
],
|
||||||
],
|
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
}
|
||||||
}
|
}
|
||||||
).populate("tags")
|
|
||||||
|
if (hasWriteOnlyAccess) {
|
||||||
|
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag")
|
||||||
|
} else {
|
||||||
|
secrets = await Secret.find(secretQuery).populate("tags")
|
||||||
}
|
}
|
||||||
|
|
||||||
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from "@sentry/node";
|
||||||
import { Octokit } from '@octokit/rest';
|
import { Octokit } from "@octokit/rest";
|
||||||
import { IIntegrationAuth } from '../models';
|
import { IIntegrationAuth } from "../models";
|
||||||
import {
|
import {
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
INTEGRATION_AWS_PARAMETER_STORE,
|
||||||
@@ -12,12 +12,14 @@ import {
|
|||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_HEROKU_API_URL,
|
INTEGRATION_HEROKU_API_URL,
|
||||||
INTEGRATION_VERCEL_API_URL,
|
INTEGRATION_VERCEL_API_URL,
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
INTEGRATION_RENDER_API_URL,
|
INTEGRATION_RENDER_API_URL,
|
||||||
INTEGRATION_FLYIO_API_URL
|
INTEGRATION_FLYIO_API_URL,
|
||||||
} from '../variables';
|
INTEGRATION_CIRCLECI_API_URL,
|
||||||
|
} from "../variables";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of names of apps for integration named [integration]
|
* Return list of names of apps for integration named [integration]
|
||||||
@@ -29,7 +31,7 @@ import {
|
|||||||
*/
|
*/
|
||||||
const getApps = async ({
|
const getApps = async ({
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
accessToken
|
accessToken,
|
||||||
}: {
|
}: {
|
||||||
integrationAuth: IIntegrationAuth;
|
integrationAuth: IIntegrationAuth;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
@@ -54,40 +56,45 @@ const getApps = async ({
|
|||||||
break;
|
break;
|
||||||
case INTEGRATION_HEROKU:
|
case INTEGRATION_HEROKU:
|
||||||
apps = await getAppsHeroku({
|
apps = await getAppsHeroku({
|
||||||
accessToken
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case INTEGRATION_VERCEL:
|
case INTEGRATION_VERCEL:
|
||||||
apps = await getAppsVercel({
|
apps = await getAppsVercel({
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
accessToken
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case INTEGRATION_NETLIFY:
|
case INTEGRATION_NETLIFY:
|
||||||
apps = await getAppsNetlify({
|
apps = await getAppsNetlify({
|
||||||
accessToken
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case INTEGRATION_GITHUB:
|
case INTEGRATION_GITHUB:
|
||||||
apps = await getAppsGithub({
|
apps = await getAppsGithub({
|
||||||
accessToken
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case INTEGRATION_RENDER:
|
case INTEGRATION_RENDER:
|
||||||
apps = await getAppsRender({
|
apps = await getAppsRender({
|
||||||
accessToken
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case INTEGRATION_FLYIO:
|
case INTEGRATION_FLYIO:
|
||||||
apps = await getAppsFlyio({
|
apps = await getAppsFlyio({
|
||||||
accessToken
|
accessToken,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_CIRCLECI:
|
||||||
|
apps = await getAppsCircleCI({
|
||||||
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get integration apps');
|
throw new Error("Failed to get integration apps");
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
@@ -106,19 +113,19 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
const res = (
|
const res = (
|
||||||
await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
|
await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/vnd.heroku+json; version=3',
|
Accept: "application/vnd.heroku+json; version=3",
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
apps = res.map((a: any) => ({
|
apps = res.map((a: any) => ({
|
||||||
name: a.name
|
name: a.name,
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get Heroku integration apps');
|
throw new Error("Failed to get Heroku integration apps");
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
@@ -131,10 +138,10 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
* @returns {Object[]} apps - names of Vercel apps
|
* @returns {Object[]} apps - names of Vercel apps
|
||||||
* @returns {String} apps.name - name of Vercel app
|
* @returns {String} apps.name - name of Vercel app
|
||||||
*/
|
*/
|
||||||
const getAppsVercel = async ({
|
const getAppsVercel = async ({
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
accessToken
|
accessToken,
|
||||||
}: {
|
}: {
|
||||||
integrationAuth: IIntegrationAuth;
|
integrationAuth: IIntegrationAuth;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -146,21 +153,23 @@ const getAppsVercel = async ({
|
|||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
'Accept-Encoding': 'application/json'
|
'Accept-Encoding': 'application/json'
|
||||||
},
|
},
|
||||||
...( integrationAuth?.teamId ? {
|
...(integrationAuth?.teamId
|
||||||
params: {
|
? {
|
||||||
teamId: integrationAuth.teamId
|
params: {
|
||||||
}
|
teamId: integrationAuth.teamId,
|
||||||
} : {})
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
apps = res.projects.map((a: any) => ({
|
apps = res.projects.map((a: any) => ({
|
||||||
name: a.name
|
name: a.name,
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get Vercel integration apps');
|
throw new Error("Failed to get Vercel integration apps");
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
@@ -173,11 +182,7 @@ const getAppsVercel = async ({
|
|||||||
* @returns {Object[]} apps - names of Netlify sites
|
* @returns {Object[]} apps - names of Netlify sites
|
||||||
* @returns {String} apps.name - name of Netlify site
|
* @returns {String} apps.name - name of Netlify site
|
||||||
*/
|
*/
|
||||||
const getAppsNetlify = async ({
|
const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
|
||||||
accessToken
|
|
||||||
}: {
|
|
||||||
accessToken: string;
|
|
||||||
}) => {
|
|
||||||
let apps;
|
let apps;
|
||||||
try {
|
try {
|
||||||
const res = (
|
const res = (
|
||||||
@@ -191,12 +196,12 @@ const getAppsNetlify = async ({
|
|||||||
|
|
||||||
apps = res.map((a: any) => ({
|
apps = res.map((a: any) => ({
|
||||||
name: a.name,
|
name: a.name,
|
||||||
appId: a.site_id
|
appId: a.site_id,
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get Netlify integration apps');
|
throw new Error("Failed to get Netlify integration apps");
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
@@ -209,35 +214,32 @@ const getAppsNetlify = async ({
|
|||||||
* @returns {Object[]} apps - names of Netlify sites
|
* @returns {Object[]} apps - names of Netlify sites
|
||||||
* @returns {String} apps.name - name of Netlify site
|
* @returns {String} apps.name - name of Netlify site
|
||||||
*/
|
*/
|
||||||
const getAppsGithub = async ({
|
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
|
||||||
accessToken
|
|
||||||
}: {
|
|
||||||
accessToken: string;
|
|
||||||
}) => {
|
|
||||||
let apps;
|
let apps;
|
||||||
try {
|
try {
|
||||||
const octokit = new Octokit({
|
const octokit = new Octokit({
|
||||||
auth: accessToken
|
auth: accessToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
const repos = (await octokit.request(
|
const repos = (
|
||||||
'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}',
|
await octokit.request(
|
||||||
{
|
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
|
||||||
per_page: 100
|
{
|
||||||
}
|
per_page: 100,
|
||||||
)).data;
|
}
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
apps = repos
|
apps = repos
|
||||||
.filter((a:any) => a.permissions.admin === true)
|
.filter((a: any) => a.permissions.admin === true)
|
||||||
.map((a: any) => ({
|
.map((a: any) => ({
|
||||||
name: a.name,
|
name: a.name,
|
||||||
owner: a.owner.login
|
owner: a.owner.login,
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get Github repos');
|
throw new Error("Failed to get Github repos");
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
@@ -251,11 +253,7 @@ const getAppsGithub = async ({
|
|||||||
* @returns {String} apps.name - name of Render service
|
* @returns {String} apps.name - name of Render service
|
||||||
* @returns {String} apps.appId - id of Render service
|
* @returns {String} apps.appId - id of Render service
|
||||||
*/
|
*/
|
||||||
const getAppsRender = async ({
|
const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
|
||||||
accessToken
|
|
||||||
}: {
|
|
||||||
accessToken: string;
|
|
||||||
}) => {
|
|
||||||
let apps: any;
|
let apps: any;
|
||||||
try {
|
try {
|
||||||
const res = (
|
const res = (
|
||||||
@@ -263,8 +261,8 @@ const getAppsRender = async ({
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Accept-Encoding': 'application/json'
|
'Accept-Encoding': 'application/json',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
@@ -277,11 +275,11 @@ const getAppsRender = async ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get Render services');
|
throw new Error("Failed to get Render services");
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of apps for Fly.io integration
|
* Return list of apps for Fly.io integration
|
||||||
@@ -290,11 +288,7 @@ const getAppsRender = async ({
|
|||||||
* @returns {Object[]} apps - names and ids of Fly.io apps
|
* @returns {Object[]} apps - names and ids of Fly.io apps
|
||||||
* @returns {String} apps.name - name of Fly.io apps
|
* @returns {String} apps.name - name of Fly.io apps
|
||||||
*/
|
*/
|
||||||
const getAppsFlyio = async ({
|
const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
|
||||||
accessToken
|
|
||||||
}: {
|
|
||||||
accessToken: string;
|
|
||||||
}) => {
|
|
||||||
let apps;
|
let apps;
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@@ -308,34 +302,71 @@ const getAppsFlyio = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const res = (await axios({
|
const res = (
|
||||||
url: INTEGRATION_FLYIO_API_URL,
|
await axios({
|
||||||
method: 'post',
|
url: INTEGRATION_FLYIO_API_URL,
|
||||||
headers: {
|
method: "post",
|
||||||
'Authorization': 'Bearer ' + accessToken,
|
headers: {
|
||||||
|
Authorization: "Bearer " + accessToken,
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Accept-Encoding': 'application/json'
|
'Accept-Encoding': 'application/json',
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
query,
|
query,
|
||||||
variables: {
|
variables: {
|
||||||
role: null
|
role: null,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})).data.data.apps.nodes;
|
})
|
||||||
|
).data.data.apps.nodes;
|
||||||
apps = res
|
|
||||||
.map((a: any) => ({
|
apps = res.map((a: any) => ({
|
||||||
name: a.name
|
name: a.name,
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to get Fly.io apps');
|
throw new Error("Failed to get Fly.io apps");
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of projects for CircleCI integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.accessToken - access token for CircleCI API
|
||||||
|
* @returns {Object[]} apps -
|
||||||
|
* @returns {String} apps.name - name of CircleCI apps
|
||||||
|
*/
|
||||||
|
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
|
||||||
|
let apps: any;
|
||||||
|
try {
|
||||||
|
const res = (
|
||||||
|
await axios.get(
|
||||||
|
`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Accept-Encoding": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data
|
||||||
|
|
||||||
|
apps = res?.map((a: any) => {
|
||||||
|
return {
|
||||||
|
name: a?.reponame
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error("Failed to get CircleCI projects");
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
}
|
};
|
||||||
|
|
||||||
export { getApps };
|
export { getApps };
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
INTEGRATION_AWS_PARAMETER_STORE,
|
||||||
@@ -8,8 +8,9 @@ import {
|
|||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO
|
INTEGRATION_FLYIO,
|
||||||
} from '../variables';
|
INTEGRATION_CIRCLECI,
|
||||||
|
} from "../variables";
|
||||||
|
|
||||||
export interface IIntegration {
|
export interface IIntegration {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
@@ -31,44 +32,47 @@ export interface IIntegration {
|
|||||||
| 'netlify'
|
| 'netlify'
|
||||||
| 'github'
|
| 'github'
|
||||||
| 'render'
|
| 'render'
|
||||||
| 'flyio';
|
| 'flyio'
|
||||||
|
| 'circleci';
|
||||||
integrationAuth: Types.ObjectId;
|
integrationAuth: Types.ObjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const integrationSchema = new Schema<IIntegration>(
|
const integrationSchema = new Schema<IIntegration>(
|
||||||
{
|
{
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Workspace',
|
ref: "Workspace",
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
isActive: {
|
isActive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
// name of app in provider
|
// name of app in provider
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
appId: { // (new)
|
appId: {
|
||||||
|
// (new)
|
||||||
// id of app in provider
|
// id of app in provider
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
targetEnvironment: { // (new)
|
targetEnvironment: {
|
||||||
// target environment
|
// (new)
|
||||||
|
// target environment
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
owner: {
|
owner: {
|
||||||
// github-specific repo owner-login
|
// github-specific repo owner-login
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
path: {
|
path: {
|
||||||
// aws-parameter-store-specific path
|
// aws-parameter-store-specific path
|
||||||
@@ -91,21 +95,22 @@ const integrationSchema = new Schema<IIntegration>(
|
|||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO
|
INTEGRATION_FLYIO,
|
||||||
|
INTEGRATION_CIRCLECI,
|
||||||
],
|
],
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
integrationAuth: {
|
integrationAuth: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'IntegrationAuth',
|
ref: "IntegrationAuth",
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const Integration = model<IIntegration>('Integration', integrationSchema);
|
const Integration = model<IIntegration>("Integration", integrationSchema);
|
||||||
|
|
||||||
export default Integration;
|
export default Integration;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
INTEGRATION_AWS_PARAMETER_STORE,
|
||||||
@@ -8,24 +8,16 @@ import {
|
|||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO
|
INTEGRATION_FLYIO,
|
||||||
} from '../variables';
|
INTEGRATION_CIRCLECI,
|
||||||
|
} from "../variables";
|
||||||
|
|
||||||
export interface IIntegrationAuth {
|
export interface IIntegrationAuth {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
integration:
|
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'aws-parameter-store' | 'aws-secret-manager';
|
||||||
| 'azure-key-vault'
|
teamId: string;
|
||||||
| 'aws-parameter-store'
|
accountId: string;
|
||||||
| 'aws-secret-manager'
|
|
||||||
| 'heroku'
|
|
||||||
| 'vercel'
|
|
||||||
| 'netlify'
|
|
||||||
| 'github'
|
|
||||||
| 'render'
|
|
||||||
| 'flyio';
|
|
||||||
teamId: string; // TODO: deprecate (vercel) -> move to accessId
|
|
||||||
accountId: string; // TODO: deprecate (netlify) -> move to accessId
|
|
||||||
refreshCiphertext?: string;
|
refreshCiphertext?: string;
|
||||||
refreshIV?: string;
|
refreshIV?: string;
|
||||||
refreshTag?: string;
|
refreshTag?: string;
|
||||||
@@ -41,9 +33,9 @@ export interface IIntegrationAuth {
|
|||||||
const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
||||||
{
|
{
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Workspace',
|
ref: "Workspace",
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
integration: {
|
integration: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -56,29 +48,30 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
|||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO
|
INTEGRATION_FLYIO,
|
||||||
|
INTEGRATION_CIRCLECI,
|
||||||
],
|
],
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
teamId: {
|
teamId: {
|
||||||
// vercel-specific integration param
|
// vercel-specific integration param
|
||||||
type: String
|
type: String,
|
||||||
},
|
},
|
||||||
accountId: {
|
accountId: {
|
||||||
// netlify-specific integration param
|
// netlify-specific integration param
|
||||||
type: String
|
type: String,
|
||||||
},
|
},
|
||||||
refreshCiphertext: {
|
refreshCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
refreshIV: {
|
refreshIV: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
refreshTag: {
|
refreshTag: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
accessIdCiphertext: {
|
accessIdCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -94,28 +87,28 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
|||||||
},
|
},
|
||||||
accessCiphertext: {
|
accessCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
accessIV: {
|
accessIV: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
accessTag: {
|
accessTag: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
accessExpiresAt: {
|
accessExpiresAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
select: false
|
select: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const IntegrationAuth = model<IIntegrationAuth>(
|
const IntegrationAuth = model<IIntegrationAuth>(
|
||||||
'IntegrationAuth',
|
"IntegrationAuth",
|
||||||
integrationAuthSchema
|
integrationAuthSchema
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,20 +17,20 @@ interface IApprover {
|
|||||||
status: ApprovalStatus;
|
status: ApprovalStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ApprovalStatus {
|
export enum ApprovalStatus {
|
||||||
PENDING = 'pending',
|
PENDING = 'pending',
|
||||||
APPROVED = 'approved',
|
APPROVED = 'approved',
|
||||||
REJECTED = 'rejected'
|
REJECTED = 'rejected'
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RequestType {
|
export enum RequestType {
|
||||||
UPDATE = 'update',
|
UPDATE = 'update',
|
||||||
DELETE = 'delete',
|
DELETE = 'delete',
|
||||||
CREATE = 'create'
|
CREATE = 'create'
|
||||||
}
|
}
|
||||||
|
|
||||||
const approverSchema = new mongoose.Schema({
|
const approverSchema = new mongoose.Schema({
|
||||||
userId: {
|
user: {
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
ref: 'User',
|
ref: 'User',
|
||||||
required: true
|
required: true
|
||||||
@@ -42,7 +42,6 @@ const approverSchema = new mongoose.Schema({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
|
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
|
||||||
{
|
{
|
||||||
secret: {
|
secret: {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ router.get(
|
|||||||
'/',
|
'/',
|
||||||
query('workspaceId').exists().trim(),
|
query('workspaceId').exists().trim(),
|
||||||
query('environment').exists().trim(),
|
query('environment').exists().trim(),
|
||||||
|
query('tagSlugs'),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
|
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
ENV_TESTING,
|
ENV_TESTING,
|
||||||
ENV_STAGING,
|
ENV_STAGING,
|
||||||
ENV_PROD,
|
ENV_PROD,
|
||||||
ENV_SET
|
ENV_SET,
|
||||||
} from './environment';
|
} from "./environment";
|
||||||
import {
|
import {
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
INTEGRATION_AWS_PARAMETER_STORE,
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
@@ -27,17 +28,12 @@ import {
|
|||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
INTEGRATION_RENDER_API_URL,
|
INTEGRATION_RENDER_API_URL,
|
||||||
INTEGRATION_FLYIO_API_URL,
|
INTEGRATION_FLYIO_API_URL,
|
||||||
INTEGRATION_OPTIONS
|
INTEGRATION_CIRCLECI_API_URL,
|
||||||
} from './integration';
|
INTEGRATION_OPTIONS,
|
||||||
import {
|
} from "./integration";
|
||||||
OWNER,
|
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization";
|
||||||
ADMIN,
|
import { SECRET_SHARED, SECRET_PERSONAL } from "./secret";
|
||||||
MEMBER,
|
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from "./event";
|
||||||
INVITED,
|
|
||||||
ACCEPTED,
|
|
||||||
} from './organization';
|
|
||||||
import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
|
|
||||||
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event';
|
|
||||||
import {
|
import {
|
||||||
ACTION_LOGIN,
|
ACTION_LOGIN,
|
||||||
ACTION_LOGOUT,
|
ACTION_LOGOUT,
|
||||||
@@ -80,6 +76,7 @@ export {
|
|||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
@@ -92,6 +89,7 @@ export {
|
|||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
INTEGRATION_RENDER_API_URL,
|
INTEGRATION_RENDER_API_URL,
|
||||||
INTEGRATION_FLYIO_API_URL,
|
INTEGRATION_FLYIO_API_URL,
|
||||||
|
INTEGRATION_CIRCLECI_API_URL,
|
||||||
EVENT_PUSH_SECRETS,
|
EVENT_PUSH_SECRETS,
|
||||||
EVENT_PULL_SECRETS,
|
EVENT_PULL_SECRETS,
|
||||||
ACTION_LOGIN,
|
ACTION_LOGIN,
|
||||||
|
|||||||
@@ -3,50 +3,53 @@ import {
|
|||||||
TENANT_ID_AZURE
|
TENANT_ID_AZURE
|
||||||
} from '../config';
|
} from '../config';
|
||||||
import {
|
import {
|
||||||
CLIENT_ID_HEROKU,
|
CLIENT_ID_HEROKU,
|
||||||
CLIENT_ID_NETLIFY,
|
CLIENT_ID_NETLIFY,
|
||||||
CLIENT_ID_GITHUB,
|
CLIENT_ID_GITHUB,
|
||||||
CLIENT_SLUG_VERCEL
|
CLIENT_SLUG_VERCEL,
|
||||||
} from '../config';
|
} from "../config";
|
||||||
|
|
||||||
// integrations
|
// integrations
|
||||||
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
|
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
|
||||||
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
|
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
|
||||||
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
|
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
|
||||||
const INTEGRATION_HEROKU = 'heroku';
|
const INTEGRATION_HEROKU = "heroku";
|
||||||
const INTEGRATION_VERCEL = 'vercel';
|
const INTEGRATION_VERCEL = "vercel";
|
||||||
const INTEGRATION_NETLIFY = 'netlify';
|
const INTEGRATION_NETLIFY = "netlify";
|
||||||
const INTEGRATION_GITHUB = 'github';
|
const INTEGRATION_GITHUB = "github";
|
||||||
const INTEGRATION_RENDER = 'render';
|
const INTEGRATION_RENDER = "render";
|
||||||
const INTEGRATION_FLYIO = 'flyio';
|
const INTEGRATION_FLYIO = "flyio";
|
||||||
|
const INTEGRATION_CIRCLECI = "circleci";
|
||||||
const INTEGRATION_SET = new Set([
|
const INTEGRATION_SET = new Set([
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO
|
INTEGRATION_FLYIO,
|
||||||
|
INTEGRATION_CIRCLECI,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// integration types
|
// integration types
|
||||||
const INTEGRATION_OAUTH2 = 'oauth2';
|
const INTEGRATION_OAUTH2 = "oauth2";
|
||||||
|
|
||||||
// integration oauth endpoints
|
// integration oauth endpoints
|
||||||
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
|
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
|
||||||
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
||||||
const INTEGRATION_VERCEL_TOKEN_URL =
|
const INTEGRATION_VERCEL_TOKEN_URL =
|
||||||
'https://api.vercel.com/v2/oauth/access_token';
|
"https://api.vercel.com/v2/oauth/access_token";
|
||||||
const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token';
|
const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token";
|
||||||
const INTEGRATION_GITHUB_TOKEN_URL =
|
const INTEGRATION_GITHUB_TOKEN_URL =
|
||||||
'https://github.com/login/oauth/access_token';
|
"https://github.com/login/oauth/access_token";
|
||||||
|
|
||||||
// integration apps endpoints
|
// integration apps endpoints
|
||||||
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
|
const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
|
||||||
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
|
const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
|
||||||
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com';
|
const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
|
||||||
const INTEGRATION_RENDER_API_URL = 'https://api.render.com';
|
const INTEGRATION_RENDER_API_URL = "https://api.render.com";
|
||||||
const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql';
|
const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
|
||||||
|
const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
|
||||||
|
|
||||||
const INTEGRATION_OPTIONS = [
|
const INTEGRATION_OPTIONS = [
|
||||||
{
|
{
|
||||||
@@ -122,6 +125,15 @@ const INTEGRATION_OPTIONS = [
|
|||||||
clientId: '',
|
clientId: '',
|
||||||
docsLink: ''
|
docsLink: ''
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Circle CI',
|
||||||
|
slug: 'circleci',
|
||||||
|
image: 'Circle CI.png',
|
||||||
|
isAvailable: true,
|
||||||
|
type: 'pat',
|
||||||
|
clientId: '',
|
||||||
|
docsLink: ''
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Azure Key Vault',
|
name: 'Azure Key Vault',
|
||||||
slug: 'azure-key-vault',
|
slug: 'azure-key-vault',
|
||||||
@@ -149,15 +161,6 @@ const INTEGRATION_OPTIONS = [
|
|||||||
type: '',
|
type: '',
|
||||||
clientId: '',
|
clientId: '',
|
||||||
docsLink: ''
|
docsLink: ''
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Circle CI',
|
|
||||||
slug: 'circleci',
|
|
||||||
image: 'Circle CI.png',
|
|
||||||
isAvailable: false,
|
|
||||||
type: '',
|
|
||||||
clientId: '',
|
|
||||||
docsLink: ''
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -165,23 +168,25 @@ export {
|
|||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
INTEGRATION_AWS_PARAMETER_STORE,
|
||||||
INTEGRATION_AWS_SECRET_MANAGER,
|
INTEGRATION_AWS_SECRET_MANAGER,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_SET,
|
||||||
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_AZURE_TOKEN_URL,
|
INTEGRATION_AZURE_TOKEN_URL,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
INTEGRATION_VERCEL_TOKEN_URL,
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
INTEGRATION_GITHUB_TOKEN_URL,
|
INTEGRATION_GITHUB_TOKEN_URL,
|
||||||
INTEGRATION_HEROKU_API_URL,
|
INTEGRATION_HEROKU_API_URL,
|
||||||
INTEGRATION_VERCEL_API_URL,
|
INTEGRATION_VERCEL_API_URL,
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
INTEGRATION_RENDER_API_URL,
|
INTEGRATION_RENDER_API_URL,
|
||||||
INTEGRATION_FLYIO_API_URL,
|
INTEGRATION_FLYIO_API_URL,
|
||||||
INTEGRATION_OPTIONS
|
INTEGRATION_CIRCLECI_API_URL,
|
||||||
|
INTEGRATION_OPTIONS,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +1,16 @@
|
|||||||
// membership roles
|
// membership roles
|
||||||
const OWNER = 'owner';
|
const OWNER = "owner";
|
||||||
const ADMIN = 'admin';
|
const ADMIN = "admin";
|
||||||
const MEMBER = 'member';
|
const MEMBER = "member";
|
||||||
|
|
||||||
// membership statuses
|
// membership statuses
|
||||||
const INVITED = 'invited';
|
const INVITED = "invited";
|
||||||
|
|
||||||
// membership permissions ability
|
// membership permissions ability
|
||||||
const ABILITY_READ = 'read';
|
const ABILITY_READ = "read";
|
||||||
const ABILITY_WRITE = 'write';
|
const ABILITY_WRITE = "write";
|
||||||
|
|
||||||
// -- organization
|
// -- organization
|
||||||
const ACCEPTED = 'accepted';
|
const ACCEPTED = "accepted";
|
||||||
|
|
||||||
export {
|
export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED, ABILITY_READ, ABILITY_WRITE };
|
||||||
OWNER,
|
|
||||||
ADMIN,
|
|
||||||
MEMBER,
|
|
||||||
INVITED,
|
|
||||||
ACCEPTED,
|
|
||||||
ABILITY_READ,
|
|
||||||
ABILITY_WRITE
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Req
|
|||||||
SetHeader("User-Agent", USER_AGENT).
|
SetHeader("User-Agent", USER_AGENT).
|
||||||
SetQueryParam("environment", request.Environment).
|
SetQueryParam("environment", request.Environment).
|
||||||
SetQueryParam("workspaceId", request.WorkspaceId).
|
SetQueryParam("workspaceId", request.WorkspaceId).
|
||||||
|
SetQueryParam("tagSlugs", request.TagSlugs).
|
||||||
Get(fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL))
|
Get(fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -154,13 +155,12 @@ func CallIsAuthenticated(httpClient *resty.Client) bool {
|
|||||||
SetHeader("User-Agent", USER_AGENT).
|
SetHeader("User-Agent", USER_AGENT).
|
||||||
Post(fmt.Sprintf("%v/v1/auth/checkAuth", config.INFISICAL_URL))
|
Post(fmt.Sprintf("%v/v1/auth/checkAuth", config.INFISICAL_URL))
|
||||||
|
|
||||||
log.Debugln(fmt.Errorf("CallIsAuthenticated: Unsuccessful response: [response=%v]", response))
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.IsError() {
|
if response.IsError() {
|
||||||
|
log.Debugln(fmt.Errorf("CallIsAuthenticated: Unsuccessful response: [response=%v]", response))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,8 +175,6 @@ func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessib
|
|||||||
SetHeader("User-Agent", USER_AGENT).
|
SetHeader("User-Agent", USER_AGENT).
|
||||||
Get(fmt.Sprintf("%v/v2/workspace/%s/environments", config.INFISICAL_URL, request.WorkspaceId))
|
Get(fmt.Sprintf("%v/v2/workspace/%s/environments", config.INFISICAL_URL, request.WorkspaceId))
|
||||||
|
|
||||||
log.Debugln(fmt.Errorf("CallGetAccessibleEnvironments: Unsuccessful response: [response=%v]", response))
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return GetAccessibleEnvironmentsResponse{}, err
|
return GetAccessibleEnvironmentsResponse{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ type GetSecretsByWorkspaceIdAndEnvironmentRequest struct {
|
|||||||
type GetEncryptedSecretsV2Request struct {
|
type GetEncryptedSecretsV2Request struct {
|
||||||
Environment string `json:"environment"`
|
Environment string `json:"environment"`
|
||||||
WorkspaceId string `json:"workspaceId"`
|
WorkspaceId string `json:"workspaceId"`
|
||||||
|
TagSlugs string `json:"tagSlugs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetEncryptedSecretsV2Response struct {
|
type GetEncryptedSecretsV2Response struct {
|
||||||
|
|||||||
@@ -61,7 +61,12 @@ var exportCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken})
|
tagSlugs, err := cmd.Flags().GetString("tags")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to fetch secrets")
|
util.HandleError(err, "Unable to fetch secrets")
|
||||||
}
|
}
|
||||||
@@ -97,6 +102,7 @@ func init() {
|
|||||||
exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)")
|
exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)")
|
||||||
exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||||
exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||||
|
exportCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format according to the format flag
|
// Format according to the format flag
|
||||||
|
|||||||
@@ -74,7 +74,12 @@ var runCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken})
|
tagSlugs, err := cmd.Flags().GetString("tags")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
|
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
|
||||||
@@ -148,6 +153,7 @@ func init() {
|
|||||||
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||||
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||||
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
|
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
|
||||||
|
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Will execute a single command and pass in the given secrets into the process
|
// Will execute a single command and pass in the given secrets into the process
|
||||||
|
|||||||
@@ -46,7 +46,12 @@ var secretsCmd = &cobra.Command{
|
|||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
|
tagSlugs, err := cmd.Flags().GetString("tags")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
}
|
}
|
||||||
@@ -342,7 +347,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
|
tagSlugs, err := cmd.Flags().GetString("tags")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "To fetch all secrets")
|
util.HandleError(err, "To fetch all secrets")
|
||||||
}
|
}
|
||||||
@@ -385,7 +395,12 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
|
tagSlugs, err := cmd.Flags().GetString("tags")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "To fetch all secrets")
|
util.HandleError(err, "To fetch all secrets")
|
||||||
}
|
}
|
||||||
@@ -567,5 +582,6 @@ func init() {
|
|||||||
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||||
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
|
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
|
||||||
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||||
|
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
|
||||||
rootCmd.AddCommand(secretsCmd)
|
rootCmd.AddCommand(secretsCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,4 +51,5 @@ type SymmetricEncryptionResult struct {
|
|||||||
type GetAllSecretsParameters struct {
|
type GetAllSecretsParameters struct {
|
||||||
Environment string
|
Environment string
|
||||||
InfisicalToken string
|
InfisicalToken string
|
||||||
|
TagSlugs string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
|
|||||||
return plainTextSecrets, nil
|
return plainTextSecrets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string) ([]models.SingleEnvironmentVariable, error) {
|
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string) ([]models.SingleEnvironmentVariable, error) {
|
||||||
httpClient := resty.New()
|
httpClient := resty.New()
|
||||||
httpClient.SetAuthToken(JTWToken).
|
httpClient.SetAuthToken(JTWToken).
|
||||||
SetHeader("Accept", "application/json")
|
SetHeader("Accept", "application/json")
|
||||||
@@ -85,6 +85,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
|
|||||||
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
|
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
|
||||||
WorkspaceId: workspaceId,
|
WorkspaceId: workspaceId,
|
||||||
Environment: environmentName,
|
Environment: environmentName,
|
||||||
|
TagSlugs: tagSlugs,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -136,7 +137,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
|
|||||||
return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
|
return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment)
|
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs)
|
||||||
log.Debugf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
|
log.Debugf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
|
||||||
|
|
||||||
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
|
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ const integrationSlugNameMapping: Mapping = {
|
|||||||
'netlify': 'Netlify',
|
'netlify': 'Netlify',
|
||||||
'github': 'GitHub',
|
'github': 'GitHub',
|
||||||
'render': 'Render',
|
'render': 'Render',
|
||||||
'flyio': 'Fly.io'
|
'flyio': 'Fly.io',
|
||||||
|
"circleci": 'CircleCI'
|
||||||
}
|
}
|
||||||
|
|
||||||
const envMapping: Mapping = {
|
const envMapping: Mapping = {
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [myRole, setMyRole] = useState('member');
|
const [myRole, setMyRole] = useState('member');
|
||||||
const [userProjectMemberships, setUserProjectMemberships] = useState<any[]>([]);
|
const [userProjectMemberships, setUserProjectMemberships] = useState<any[]>([]);
|
||||||
console.log(123, userData)
|
|
||||||
|
|
||||||
const workspaceId = router.query.id as string;
|
const workspaceId = router.query.id as string;
|
||||||
// Delete the row in the table (e.g. a user)
|
// Delete the row in the table (e.g. a user)
|
||||||
@@ -198,15 +197,15 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
<td className="flex items-center max-h-16 overflow-x-auto w-full max-w-xl">
|
<div className="flex items-center max-h-16 overflow-x-auto w-full max-w-xl break-all">
|
||||||
{userProjectMemberships[row.userId]
|
{userProjectMemberships[row.userId]
|
||||||
? userProjectMemberships[row.userId]?.map((project: any) => (
|
? userProjectMemberships[row.userId]?.map((project: any) => (
|
||||||
<div key={project.id} className='mx-1 min-w-max px-1.5 bg-mineshaft-500 rounded-sm text-sm text-bunker-200 flex items-center'>
|
<div key={project._id} className='mx-1 min-w-max px-1.5 bg-mineshaft-500 rounded-sm text-sm text-bunker-200 flex items-center'>
|
||||||
<span className='mb-0.5 cursor-default'>{project.name}</span>
|
<span className='mb-0.5 cursor-default'>{project.name}</span>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
: <span className='ml-1 text-bunker-100 rounded-sm px-1 py-0.5 text-sm bg-red/80'>This user isn't part of any projects yet.</span>}
|
: <span className='ml-1 text-bunker-100 rounded-sm px-1 py-0.5 text-sm bg-red/80'>This user isn't part of any projects yet.</span>}
|
||||||
</td>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
||||||
{myUser !== row.email &&
|
{myUser !== row.email &&
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ const CloudIntegration = ({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={`relative ${
|
className={`relative ${
|
||||||
cloudIntegrationOption.isAvailable
|
cloudIntegrationOption.isAvailable
|
||||||
? 'hover:bg-white/10 duration-200 cursor-pointer'
|
? 'cursor-pointer duration-200 hover:bg-white/10'
|
||||||
: 'opacity-50'
|
: 'opacity-50'
|
||||||
} flex flex-row bg-white/5 h-32 rounded-md p-4 items-center`}
|
} flex h-32 flex-row items-center rounded-md bg-white/5 p-4`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!cloudIntegrationOption.isAvailable) return;
|
if (!cloudIntegrationOption.isAvailable) return;
|
||||||
setSelectedIntegrationOption(cloudIntegrationOption);
|
setSelectedIntegrationOption(cloudIntegrationOption);
|
||||||
@@ -61,22 +61,22 @@ const CloudIntegration = ({
|
|||||||
alt="integration logo"
|
alt="integration logo"
|
||||||
/>
|
/>
|
||||||
{cloudIntegrationOption.name.split(' ').length > 2 ? (
|
{cloudIntegrationOption.name.split(' ').length > 2 ? (
|
||||||
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs">
|
<div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||||
<div>{cloudIntegrationOption.name.split(' ')[0]}</div>
|
<div>{cloudIntegrationOption.name.split(' ')[0]}</div>
|
||||||
<div className="text-base">
|
<div className="text-base">
|
||||||
{cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
|
{cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs">
|
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||||
{cloudIntegrationOption.name}
|
{cloudIntegrationOption.name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{cloudIntegrationOption.isAvailable &&
|
{cloudIntegrationOption.isAvailable &&
|
||||||
integrationAuths
|
integrationAuths
|
||||||
.map((authorization) => authorization.integration)
|
.map((authorization) => authorization?.integration)
|
||||||
.includes(cloudIntegrationOption.slug) && (
|
.includes(cloudIntegrationOption.slug) && (
|
||||||
<div className="absolute group z-40 top-0 right-0 flex flex-row">
|
<div className="group absolute top-0 right-0 z-40 flex flex-row">
|
||||||
<div
|
<div
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
role="button"
|
role="button"
|
||||||
@@ -86,8 +86,7 @@ const CloudIntegration = ({
|
|||||||
const deletedIntegrationAuth = await deleteIntegrationAuth({
|
const deletedIntegrationAuth = await deleteIntegrationAuth({
|
||||||
integrationAuthId: integrationAuths
|
integrationAuthId: integrationAuths
|
||||||
.filter(
|
.filter(
|
||||||
(authorization) =>
|
(authorization) => authorization.integration === cloudIntegrationOption.slug
|
||||||
authorization.integration === cloudIntegrationOption.slug
|
|
||||||
)
|
)
|
||||||
.map((authorization) => authorization._id)[0]
|
.map((authorization) => authorization._id)[0]
|
||||||
});
|
});
|
||||||
@@ -96,20 +95,20 @@ const CloudIntegration = ({
|
|||||||
integrationAuth: deletedIntegrationAuth
|
integrationAuth: deletedIntegrationAuth
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200"
|
className="flex w-max cursor-pointer flex-row items-center rounded-b-md bg-red py-0.5 px-2 text-xs opacity-0 duration-200 group-hover:opacity-100"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faX} className="text-xs mr-2 py-px" />
|
<FontAwesomeIcon icon={faX} className="mr-2 py-px text-xs" />
|
||||||
Revoke
|
Revoke
|
||||||
</div>
|
</div>
|
||||||
<div className="w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200">
|
<div className="flex w-max flex-row items-center rounded-bl-md rounded-tr-md bg-primary py-0.5 px-2 text-xs text-black opacity-90 duration-200 group-hover:opacity-100">
|
||||||
<FontAwesomeIcon icon={faCheck} className="text-xs mr-2" />
|
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
|
||||||
Authorized
|
Authorized
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!cloudIntegrationOption.isAvailable && (
|
{!cloudIntegrationOption.isAvailable && (
|
||||||
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
<div className="group absolute top-0 right-0 z-50 flex flex-row">
|
||||||
<div className="w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90">
|
<div className="flex w-max flex-row items-center rounded-bl-md rounded-tr-md bg-yellow py-0.5 px-2 text-xs text-black opacity-90">
|
||||||
Coming Soon
|
Coming Soon
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ type Props = {
|
|||||||
handleDeleteIntegration: (args: { integration: Integration }) => void;
|
handleDeleteIntegration: (args: { integration: Integration }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IntegrationTile = ({
|
const IntegrationTile = ({
|
||||||
integration,
|
integration,
|
||||||
integrations,
|
integrations,
|
||||||
bot,
|
bot,
|
||||||
setBot,
|
setBot,
|
||||||
@@ -57,7 +57,7 @@ const IntegrationTile = ({
|
|||||||
|
|
||||||
// set initial environment. This find will only execute when component is mounting
|
// set initial environment. This find will only execute when component is mounting
|
||||||
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
||||||
environments.find(({ slug }) => slug === integration.environment) || {
|
environments.find(({ slug }) => slug === integration?.environment) || {
|
||||||
name: '',
|
name: '',
|
||||||
slug: ''
|
slug: ''
|
||||||
}
|
}
|
||||||
@@ -69,11 +69,10 @@ const IntegrationTile = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadIntegration = async () => {
|
const loadIntegration = async () => {
|
||||||
|
|
||||||
const tempApps: [IntegrationApp] = await getIntegrationApps({
|
const tempApps: [IntegrationApp] = await getIntegrationApps({
|
||||||
integrationAuthId: integration.integrationAuth
|
integrationAuthId: integration?.integrationAuth
|
||||||
});
|
});
|
||||||
|
|
||||||
setApps(tempApps);
|
setApps(tempApps);
|
||||||
|
|
||||||
if (integration?.app) {
|
if (integration?.app) {
|
||||||
@@ -90,15 +89,16 @@ const IntegrationTile = ({
|
|||||||
case 'vercel':
|
case 'vercel':
|
||||||
setIntegrationTargetEnvironment(
|
setIntegrationTargetEnvironment(
|
||||||
integration?.targetEnvironment
|
integration?.targetEnvironment
|
||||||
? integration.targetEnvironment.charAt(0).toUpperCase() + integration.targetEnvironment.substring(1)
|
? integration.targetEnvironment.charAt(0).toUpperCase() +
|
||||||
: 'Development'
|
integration.targetEnvironment.substring(1)
|
||||||
|
: 'Development'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'netlify':
|
case 'netlify':
|
||||||
setIntegrationTargetEnvironment(
|
setIntegrationTargetEnvironment(
|
||||||
integration?.targetEnvironment
|
integration?.targetEnvironment
|
||||||
? contextNetlifyMapping[integration.targetEnvironment]
|
? contextNetlifyMapping[integration.targetEnvironment]
|
||||||
: 'Local development'
|
: 'Local development'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -108,7 +108,7 @@ const IntegrationTile = ({
|
|||||||
|
|
||||||
loadIntegration();
|
loadIntegration();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleStartIntegration = async () => {
|
const handleStartIntegration = async () => {
|
||||||
const reformatTargetEnvironment = (targetEnvironment: string) => {
|
const reformatTargetEnvironment = (targetEnvironment: string) => {
|
||||||
switch (integration.integration) {
|
switch (integration.integration) {
|
||||||
@@ -119,13 +119,13 @@ const IntegrationTile = ({
|
|||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
||||||
const appId = siteApp?.appId ?? null;
|
const appId = siteApp?.appId ?? null;
|
||||||
const owner = siteApp?.owner ?? null;
|
const owner = siteApp?.owner ?? null;
|
||||||
|
|
||||||
// return updated integration
|
// return updated integration
|
||||||
const updatedIntegration = await updateIntegration({
|
const updatedIntegration = await updateIntegration({
|
||||||
integrationId: integration._id,
|
integrationId: integration._id,
|
||||||
@@ -136,15 +136,15 @@ const IntegrationTile = ({
|
|||||||
targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment),
|
targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment),
|
||||||
owner
|
owner
|
||||||
});
|
});
|
||||||
|
|
||||||
setIntegrations(
|
setIntegrations(
|
||||||
integrations.map((i) => i._id === updatedIntegration._id ? updatedIntegration : i)
|
integrations.map((i) => (i._id === updatedIntegration._id ? updatedIntegration : i))
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const renderIntegrationSpecificParams = (integration: Integration) => {
|
const renderIntegrationSpecificParams = (integration: Integration) => {
|
||||||
try {
|
try {
|
||||||
@@ -152,7 +152,7 @@ const IntegrationTile = ({
|
|||||||
case 'vercel':
|
case 'vercel':
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">ENVIRONMENT</div>
|
<div className="mb-2 w-60 text-xs font-semibold text-gray-400">ENVIRONMENT</div>
|
||||||
<ListBox
|
<ListBox
|
||||||
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
|
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
|
||||||
isSelected={integrationTargetEnvironment}
|
isSelected={integrationTargetEnvironment}
|
||||||
@@ -164,7 +164,7 @@ const IntegrationTile = ({
|
|||||||
case 'netlify':
|
case 'netlify':
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-400 text-xs font-semibold mb-2">CONTEXT</div>
|
<div className="mb-2 text-xs font-semibold text-gray-400">CONTEXT</div>
|
||||||
<ListBox
|
<ListBox
|
||||||
data={
|
data={
|
||||||
!integration.isActive
|
!integration.isActive
|
||||||
@@ -189,10 +189,10 @@ const IntegrationTile = ({
|
|||||||
if (!integrationApp) return <div />;
|
if (!integrationApp) return <div />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
|
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-white/5 p-6">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p>
|
<p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p>
|
||||||
<ListBox
|
<ListBox
|
||||||
data={!integration.isActive ? environments.map(({ name }) => name) : null}
|
data={!integration.isActive ? environments.map(({ name }) => name) : null}
|
||||||
isSelected={integrationEnvironment.name}
|
isSelected={integrationEnvironment.name}
|
||||||
@@ -208,7 +208,7 @@ const IntegrationTile = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400 mt-8" />
|
<FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-2">
|
<div className="mr-2">
|
||||||
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
|
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
|
||||||
@@ -218,7 +218,7 @@ const IntegrationTile = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-2">
|
<div className="mr-2">
|
||||||
<div className="text-gray-400 text-xs font-semibold mb-2">APP</div>
|
<div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
|
||||||
<ListBox
|
<ListBox
|
||||||
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
||||||
isSelected={integrationApp}
|
isSelected={integrationApp}
|
||||||
@@ -231,9 +231,9 @@ const IntegrationTile = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
{integration.isActive ? (
|
{integration.isActive ? (
|
||||||
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
|
<div className="flex max-w-5xl flex-row items-center rounded-md bg-white/5 p-2 px-4">
|
||||||
<FontAwesomeIcon icon={faRotate} className="text-lg mr-2.5 text-primary animate-spin" />
|
<FontAwesomeIcon icon={faRotate} className="mr-2.5 animate-spin text-lg text-primary" />
|
||||||
<div className="text-gray-300 font-semibold">In Sync</div>
|
<div className="font-semibold text-gray-300">In Sync</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@@ -243,11 +243,13 @@ const IntegrationTile = ({
|
|||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
|
<div className="ml-2 opacity-50 duration-200 hover:opacity-100">
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => handleDeleteIntegration({
|
onButtonPressed={() =>
|
||||||
integration
|
handleDeleteIntegration({
|
||||||
})}
|
integration
|
||||||
|
})
|
||||||
|
}
|
||||||
color="red"
|
color="red"
|
||||||
size="icon-md"
|
size="icon-md"
|
||||||
icon={faX}
|
icon={faX}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ interface Integration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ProjectIntegrationSection = ({
|
const ProjectIntegrationSection = ({
|
||||||
integrations,
|
integrations,
|
||||||
setIntegrations,
|
setIntegrations,
|
||||||
bot,
|
bot,
|
||||||
setBot,
|
setBot,
|
||||||
@@ -35,22 +35,20 @@ const ProjectIntegrationSection = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
return integrations.length > 0 ? (
|
return integrations.length > 0 ? (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
<div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2">
|
<div className="mx-4 mb-4 mt-6 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
||||||
<h1 className="font-semibold text-3xl">Current Integrations</h1>
|
<h1 className="text-3xl font-semibold">Current Integrations</h1>
|
||||||
<p className="text-base text-gray-400">
|
<p className="text-base text-gray-400">Manage integrations with third-party services.</p>
|
||||||
Manage integrations with third-party services.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{integrations.map((integration: Integration) => {
|
{integrations.map((integration: Integration) => {
|
||||||
return (
|
return (
|
||||||
<IntegrationTile
|
<IntegrationTile
|
||||||
key={`integration-${integration._id.toString()}`}
|
key={`integration-${integration?._id.toString()}`}
|
||||||
integration={integration}
|
integration={integration}
|
||||||
integrations={integrations}
|
integrations={integrations}
|
||||||
bot={bot}
|
bot={bot}
|
||||||
setBot={setBot}
|
setBot={setBot}
|
||||||
setIntegrations={setIntegrations}
|
setIntegrations={setIntegrations}
|
||||||
environments={environments}
|
environments={environments}
|
||||||
handleDeleteIntegration={handleDeleteIntegration}
|
handleDeleteIntegration={handleDeleteIntegration}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -59,6 +57,6 @@ const ProjectIntegrationSection = ({
|
|||||||
) : (
|
) : (
|
||||||
<div />
|
<div />
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ProjectIntegrationSection;
|
export default ProjectIntegrationSection;
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ export const Secondary: Story = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Star: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Hello Infisical',
|
||||||
|
variant: 'star'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const Danger: Story = {
|
export const Danger: Story = {
|
||||||
args: {
|
args: {
|
||||||
children: 'Hello Infisical',
|
children: 'Hello Infisical',
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ const buttonVariants = cva(
|
|||||||
variant: {
|
variant: {
|
||||||
solid: '',
|
solid: '',
|
||||||
outline: ['bg-transparent', 'border-2', 'border-solid'],
|
outline: ['bg-transparent', 'border-2', 'border-solid'],
|
||||||
plain: ''
|
plain: '',
|
||||||
|
// a constant color not in use on hover or click goes colorSchema color
|
||||||
|
star: 'text-bunker-200 bg-mineshaft-500'
|
||||||
},
|
},
|
||||||
isDisabled: {
|
isDisabled: {
|
||||||
true: 'bg-mineshaft opacity-40 cursor-not-allowed',
|
true: 'bg-mineshaft opacity-40 cursor-not-allowed',
|
||||||
@@ -53,6 +55,16 @@ const buttonVariants = cva(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
compoundVariants: [
|
compoundVariants: [
|
||||||
|
{
|
||||||
|
colorSchema: 'primary',
|
||||||
|
variant: 'star',
|
||||||
|
className: 'hover:bg-primary hover:text-black'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colorSchema: 'danger',
|
||||||
|
variant: 'star',
|
||||||
|
className: 'hover:bg-red hover:text-white'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
colorSchema: 'primary',
|
colorSchema: 'primary',
|
||||||
variant: 'outline',
|
variant: 'outline',
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 border border-mineshaft-500 drop-shadow-xl font-inter text-bunker-100 shadow-md z-[100]',
|
'relative top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
|
||||||
dropdownContainerClassName
|
dropdownContainerClassName
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
|
|||||||
@@ -44,3 +44,5 @@ const plansProd: Mapping = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const plans = plansProd || plansDev;
|
export const plans = plansProd || plansDev;
|
||||||
|
|
||||||
|
export const leaveConfirmDefaultMessage = 'Do you want to save your results before leaving this page?';
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const OrgProvider = ({ children }: Props): JSX.Element => {
|
|||||||
const value = useMemo<TOrgContext>(
|
const value = useMemo<TOrgContext>(
|
||||||
() => ({
|
() => ({
|
||||||
orgs: userOrgs,
|
orgs: userOrgs,
|
||||||
currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID),
|
currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID) || (userOrgs || [])[0],
|
||||||
isLoading
|
isLoading
|
||||||
}),
|
}),
|
||||||
[currentWsOrgID, userOrgs, isLoading]
|
[currentWsOrgID, userOrgs, isLoading]
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import getActionData from '@app/ee/api/secrets/GetActionData';
|
|||||||
import patienceDiff from '@app/ee/utilities/findTextDifferences';
|
import patienceDiff from '@app/ee/utilities/findTextDifferences';
|
||||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||||
|
|
||||||
import DashboardInputField from '../../components/dashboard/DashboardInputField';
|
|
||||||
import {
|
import {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
decryptSymmetric
|
decryptSymmetric
|
||||||
@@ -130,7 +129,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
|||||||
<div
|
<div
|
||||||
className={`absolute border-l border-mineshaft-500 ${
|
className={`absolute border-l border-mineshaft-500 ${
|
||||||
isLoading ? 'bg-bunker-800' : 'bg-bunker'
|
isLoading ? 'bg-bunker-800' : 'bg-bunker'
|
||||||
} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}
|
} fixed h-[calc(100vh-56px)] w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center h-full mb-8">
|
<div className="flex items-center justify-center h-full mb-8">
|
||||||
@@ -142,7 +141,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-min overflow-y-auto">
|
<div className="h-min">
|
||||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||||
<p className="font-semibold text-lg text-bunker-200">
|
<p className="font-semibold text-lg text-bunker-200">
|
||||||
{t(`activity:event.${actionMetaData?.name}`)}
|
{t(`activity:event.${actionMetaData?.name}`)}
|
||||||
@@ -157,7 +156,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
|||||||
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col px-4">
|
<div className="flex flex-col px-4 overflow-y-auto h-[calc(100vh-120px)] overflow-y-autp">
|
||||||
{(actionMetaData?.name === 'readSecrets' ||
|
{(actionMetaData?.name === 'readSecrets' ||
|
||||||
actionMetaData?.name === 'addSecrets' ||
|
actionMetaData?.name === 'addSecrets' ||
|
||||||
actionMetaData?.name === 'deleteSecrets') &&
|
actionMetaData?.name === 'deleteSecrets') &&
|
||||||
@@ -166,14 +165,9 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
|||||||
<div className="text-xs text-bunker-200 mt-4 pl-1 ph-no-capture">
|
<div className="text-xs text-bunker-200 mt-4 pl-1 ph-no-capture">
|
||||||
{item.newSecretVersion.key}
|
{item.newSecretVersion.key}
|
||||||
</div>
|
</div>
|
||||||
<DashboardInputField
|
<div className='w-full font-mono text-sm break-all bg-mineshaft-600 px-2 py-0.5 rounded-md border border-mineshaft-500 text-bunker-200'>
|
||||||
onChangeHandler={() => {}}
|
{item.newSecretVersion.value ? <span> {item.newSecretVersion.value} </span> : <span className='text-bunker-400'> EMPTY </span>}
|
||||||
type="value"
|
</div>
|
||||||
position={1}
|
|
||||||
value={item.newSecretVersion.value}
|
|
||||||
isDuplicate={false}
|
|
||||||
blurred={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{actionMetaData?.name === 'updateSecrets' &&
|
{actionMetaData?.name === 'updateSecrets' &&
|
||||||
@@ -182,8 +176,8 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
|||||||
<div className="text-xs text-bunker-200 mt-4 pl-1">
|
<div className="text-xs text-bunker-200 mt-4 pl-1">
|
||||||
{item.newSecretVersion.key}
|
{item.newSecretVersion.key}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-bunker-100 font-mono rounded-md overflow-hidden">
|
<div className="break-all text-bunker-200 font-mono rounded-md overflow-hidden border border-mineshaft-500">
|
||||||
<div className="bg-red/30 px-2 ph-no-capture">
|
<div className="bg-red/40 px-2 ph-no-capture">
|
||||||
-{' '}
|
-{' '}
|
||||||
{patienceDiff(
|
{patienceDiff(
|
||||||
item.oldSecretVersion.value.split(''),
|
item.oldSecretVersion.value.split(''),
|
||||||
@@ -194,14 +188,14 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
|||||||
character.bIndex !== -1 && (
|
character.bIndex !== -1 && (
|
||||||
<span
|
<span
|
||||||
key={`actionData.${id + 1}.line.${lineId + 1}`}
|
key={`actionData.${id + 1}.line.${lineId + 1}`}
|
||||||
className={`${character.aIndex === -1 && 'bg-red-700/80'}`}
|
className={`${character.aIndex === -1 && 'text-bunker-100 bg-red-700/80'}`}
|
||||||
>
|
>
|
||||||
{character.line}
|
{character.line}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-green-500/30 px-2 ph-no-capture">
|
<div className="break-all bg-green-500/40 px-2 ph-no-capture">
|
||||||
+{' '}
|
+{' '}
|
||||||
{patienceDiff(
|
{patienceDiff(
|
||||||
item.oldSecretVersion.value.split(''),
|
item.oldSecretVersion.value.split(''),
|
||||||
@@ -212,7 +206,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
|||||||
character.aIndex !== -1 && (
|
character.aIndex !== -1 && (
|
||||||
<span
|
<span
|
||||||
key={`actionData.${id + 1}.linev2.${lineId + 1}`}
|
key={`actionData.${id + 1}.linev2.${lineId + 1}`}
|
||||||
className={`${character.bIndex === -1 && 'bg-green-700/80'}`}
|
className={`${character.bIndex === -1 && 'text-bunker-100 bg-green-700/80'}`}
|
||||||
>
|
>
|
||||||
{character.line}
|
{character.line}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
export { useLeaveConfirm } from './useLeaveConfirm';
|
||||||
export { usePopUp } from './usePopUp';
|
export { usePopUp } from './usePopUp';
|
||||||
export { useToggle } from './useToggle';
|
export { useToggle } from './useToggle';
|
||||||
|
|||||||
55
frontend/src/hooks/useLeaveConfirm.tsx
Normal file
55
frontend/src/hooks/useLeaveConfirm.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
import { leaveConfirmDefaultMessage } from '@app/const';
|
||||||
|
|
||||||
|
type LeaveConfirmProps = {
|
||||||
|
initialValue: boolean,
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LeaveConfirmReturn {
|
||||||
|
hasUnsavedChanges: boolean,
|
||||||
|
setHasUnsavedChanges: Dispatch<SetStateAction<boolean>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLeaveConfirm({
|
||||||
|
initialValue,
|
||||||
|
message = leaveConfirmDefaultMessage,
|
||||||
|
}: LeaveConfirmProps): LeaveConfirmReturn {
|
||||||
|
const router = useRouter()
|
||||||
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(initialValue);
|
||||||
|
|
||||||
|
const onRouteChangeStart = useCallback(() => {
|
||||||
|
if (hasUnsavedChanges) {
|
||||||
|
if (window.confirm(message)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
throw new Error("Abort route change by user's confirmation.")
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, [hasUnsavedChanges])
|
||||||
|
|
||||||
|
const handleWindowClose = useCallback((e: any) => {
|
||||||
|
if (!hasUnsavedChanges) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = message;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
router.events.on("routeChangeStart", onRouteChangeStart);
|
||||||
|
window.addEventListener('beforeunload', handleWindowClose);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
router.events.off("routeChangeStart", onRouteChangeStart);
|
||||||
|
window.removeEventListener('beforeunload', handleWindowClose);
|
||||||
|
}
|
||||||
|
}, [onRouteChangeStart, handleWindowClose]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasUnsavedChanges,
|
||||||
|
setHasUnsavedChanges,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -64,7 +64,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
// eslint-disable-next-line prefer-const
|
// eslint-disable-next-line prefer-const
|
||||||
let { workspaces, currentWorkspace } = useWorkspace();
|
let { workspaces, currentWorkspace } = useWorkspace();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
workspaces = workspaces.filter(ws => ws.organization === currentOrg?._id)
|
workspaces = workspaces.filter((ws) => ws.organization === currentOrg?._id);
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
const createWs = useCreateWorkspace();
|
const createWs = useCreateWorkspace();
|
||||||
@@ -98,7 +98,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
const putUserInWorkSpace = async () => {
|
const putUserInWorkSpace = async () => {
|
||||||
if (tempLocalStorage('orgData.id') === '') {
|
if (tempLocalStorage('orgData.id') === '') {
|
||||||
const userOrgs = await getOrganizations();
|
const userOrgs = await getOrganizations();
|
||||||
localStorage.setItem('orgData.id', userOrgs[0]._id);
|
localStorage.setItem('orgData.id', userOrgs[0]?._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const orgUserProjects = await getOrganizationUserProjects({
|
const orgUserProjects = await getOrganizationUserProjects({
|
||||||
@@ -124,7 +124,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
|
|
||||||
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
||||||
if (
|
if (
|
||||||
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) &&
|
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) && userWorkspaces[0]?._id !== undefined &&
|
||||||
!userWorkspaces
|
!userWorkspaces
|
||||||
.map((workspace: { _id: string }) => workspace._id)
|
.map((workspace: { _id: string }) => workspace._id)
|
||||||
.includes(intendedWorkspaceId)
|
.includes(intendedWorkspaceId)
|
||||||
@@ -219,50 +219,58 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
<aside className="w-full border-r border-mineshaft-500 bg-mineshaft-900 md:w-60">
|
<aside className="w-full border-r border-mineshaft-500 bg-mineshaft-900 md:w-60">
|
||||||
<nav className="items-between flex h-full flex-col justify-between">
|
<nav className="items-between flex h-full flex-col justify-between">
|
||||||
<div>
|
<div>
|
||||||
{currentWorkspace
|
{currentWorkspace ? (
|
||||||
? <div className="w-full p-4 mt-3 mb-4">
|
<div className="w-full p-4 mt-3 mb-4">
|
||||||
<p className="text-xs font-semibold ml-1.5 mb-1 uppercase text-gray-400">Project</p>
|
<p className="text-xs font-semibold ml-1.5 mb-1 uppercase text-gray-400">
|
||||||
<Select
|
Project
|
||||||
defaultValue={currentWorkspace?._id}
|
</p>
|
||||||
value={currentWorkspace?._id}
|
<Select
|
||||||
className="w-full py-2.5 bg-mineshaft-600 font-medium"
|
defaultValue={currentWorkspace?._id}
|
||||||
onValueChange={(value) => {
|
value={currentWorkspace?._id}
|
||||||
router.push(`/dashboard/${value}`);
|
className="w-full py-2.5 bg-mineshaft-600 font-medium"
|
||||||
}}
|
onValueChange={(value) => {
|
||||||
position="popper"
|
router.push(`/dashboard/${value}`);
|
||||||
dropdownContainerClassName="left-0 text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50"
|
}}
|
||||||
>
|
position="popper"
|
||||||
{workspaces.map(({ _id, name }) => (
|
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50"
|
||||||
<SelectItem key={`ws-layout-list-${_id}`} value={_id} className={`${currentWorkspace?._id === _id && "bg-mineshaft-600"}`}>
|
>
|
||||||
{name}
|
{workspaces.map(({ _id, name }) => (
|
||||||
</SelectItem>
|
<SelectItem
|
||||||
))}
|
key={`ws-layout-list-${_id}`}
|
||||||
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
|
value={_id}
|
||||||
<div className="w-full">
|
className={`${currentWorkspace?._id === _id && 'bg-mineshaft-600'}`}
|
||||||
<Button
|
>
|
||||||
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
|
{name}
|
||||||
color="mineshaft"
|
</SelectItem>
|
||||||
size="sm"
|
))}
|
||||||
onClick={() => handlePopUpOpen('addNewWs')}
|
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
<div className="w-full">
|
||||||
>
|
<Button
|
||||||
Add Project
|
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
|
||||||
</Button>
|
color="mineshaft"
|
||||||
</div>
|
size="sm"
|
||||||
</Select>
|
onClick={() => handlePopUpOpen('addNewWs')}
|
||||||
</div>
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
: <div className="w-full p-4 mt-3 mb-4">
|
>
|
||||||
<Button
|
Add Project
|
||||||
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
|
</Button>
|
||||||
color="mineshaft"
|
</div>
|
||||||
size="sm"
|
</Select>
|
||||||
onClick={() => handlePopUpOpen('addNewWs')}
|
</div>
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
) : (
|
||||||
>
|
<div className="w-full p-4 mt-3 mb-4">
|
||||||
Add Project
|
<Button
|
||||||
</Button>
|
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
|
||||||
</div>}
|
color="mineshaft"
|
||||||
<div className={`${currentWorkspace ? "block" : "hidden"}`}>
|
size="sm"
|
||||||
|
onClick={() => handlePopUpOpen('addNewWs')}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add Project
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`${currentWorkspace ? 'block' : 'hidden'}`}>
|
||||||
<Menu>
|
<Menu>
|
||||||
<Link href={`/dashboard/${currentWorkspace?._id}`} passHref>
|
<Link href={`/dashboard/${currentWorkspace?._id}`} passHref>
|
||||||
<a>
|
<a>
|
||||||
@@ -393,7 +401,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className='pl-1 mt-4'>
|
<div className="pl-1 mt-4">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="addMembers"
|
name="addMembers"
|
||||||
@@ -415,11 +423,19 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
key="layout-create-project-submit"
|
key="layout-create-project-submit"
|
||||||
className=""
|
className="mr-4"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create Project
|
Create Project
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
key="layout-cancel-create-project"
|
||||||
|
onClick={() => handlePopUpClose('addNewWs')}
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@@ -35,9 +35,11 @@ import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
|
|||||||
import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject';
|
import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject';
|
||||||
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||||
import { IconButton } from '@app/components/v2';
|
import { IconButton } from '@app/components/v2';
|
||||||
|
import { leaveConfirmDefaultMessage } from '@app/const';
|
||||||
import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercetSnapshotsCount';
|
import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercetSnapshotsCount';
|
||||||
import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback';
|
import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback';
|
||||||
import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar';
|
import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar';
|
||||||
|
import { useLeaveConfirm } from '@app/hooks';
|
||||||
|
|
||||||
import addSecrets from '../api/files/AddSecrets';
|
import addSecrets from '../api/files/AddSecrets';
|
||||||
import deleteSecrets from '../api/files/DeleteSecrets';
|
import deleteSecrets from '../api/files/DeleteSecrets';
|
||||||
@@ -116,7 +118,6 @@ function findDuplicates(arr: any[]) {
|
|||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [data, setData] = useState<SecretDataProps[] | null>();
|
const [data, setData] = useState<SecretDataProps[] | null>();
|
||||||
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
|
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
|
||||||
const [buttonReady, setButtonReady] = useState(false);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [blurred, setBlurred] = useState(true);
|
const [blurred, setBlurred] = useState(true);
|
||||||
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
|
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
|
||||||
@@ -137,6 +138,7 @@ export default function Dashboard() {
|
|||||||
const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>();
|
const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>();
|
||||||
const [projectTags, setProjectTags] = useState<Tag[]>([]);
|
const [projectTags, setProjectTags] = useState<Tag[]>([]);
|
||||||
|
|
||||||
|
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({initialValue: false});
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
@@ -153,36 +155,6 @@ export default function Dashboard() {
|
|||||||
setAtSecretsAreaTop(false);
|
setAtSecretsAreaTop(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// #TODO: fix save message for changing reroutes
|
|
||||||
// const beforeRouteHandler = (url) => {
|
|
||||||
// const warningText =
|
|
||||||
// "Do you want to save your results bfore leaving this page?";
|
|
||||||
// if (!buttonReady) return;
|
|
||||||
// if (router.asPath !== url && !confirm(warningText)) {
|
|
||||||
// // router.events.emit('routeChangeError');
|
|
||||||
// // setData(data)
|
|
||||||
// savePush();
|
|
||||||
// throw `Route change to "${url}" was aborted (this error can be safely ignored).`;
|
|
||||||
// } else {
|
|
||||||
// setButtonReady(false);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// prompt the user if they try and leave with unsaved changes
|
|
||||||
useEffect(() => {
|
|
||||||
const warningText = 'Do you want to save your results before leaving this page?';
|
|
||||||
const handleWindowClose = (e: any) => {
|
|
||||||
if (!buttonReady) return;
|
|
||||||
e.preventDefault();
|
|
||||||
e.returnValue = warningText;
|
|
||||||
};
|
|
||||||
window.addEventListener('beforeunload', handleWindowClose);
|
|
||||||
// router.events.on('routeChangeStart', beforeRouteHandler);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('beforeunload', handleWindowClose);
|
|
||||||
// router.events.off('routeChangeStart', beforeRouteHandler);
|
|
||||||
};
|
|
||||||
}, [buttonReady]);
|
|
||||||
|
|
||||||
// TODO(akhilmhdh): change to FP
|
// TODO(akhilmhdh): change to FP
|
||||||
const sortValuesHandler = (
|
const sortValuesHandler = (
|
||||||
@@ -318,7 +290,7 @@ export default function Dashboard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => {
|
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => {
|
||||||
setButtonReady(true);
|
setHasUnsavedChanges(true);
|
||||||
toggleSidebar('None');
|
toggleSidebar('None');
|
||||||
createNotification({
|
createNotification({
|
||||||
text: `${secretName || 'Secret'} has been deleted. Remember to save changes.`,
|
text: `${secretName || 'Secret'} has been deleted. Remember to save changes.`,
|
||||||
@@ -332,27 +304,27 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
const modifyValue = (value: string, pos: number) => {
|
const modifyValue = (value: string, pos: number) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, value } : e)));
|
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, value } : e)));
|
||||||
setButtonReady(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyValueOverride = (valueOverride: string | undefined, pos: number) => {
|
const modifyValueOverride = (valueOverride: string | undefined, pos: number) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, valueOverride } : e)));
|
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, valueOverride } : e)));
|
||||||
setButtonReady(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyKey = (key: string, pos: number) => {
|
const modifyKey = (key: string, pos: number) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, key } : e)));
|
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, key } : e)));
|
||||||
setButtonReady(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyComment = (comment: string, pos: number) => {
|
const modifyComment = (comment: string, pos: number) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, comment } : e)));
|
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, comment } : e)));
|
||||||
setButtonReady(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyTags = (tags: Tag[], pos: number) => {
|
const modifyTags = (tags: Tag[], pos: number) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e)));
|
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e)));
|
||||||
setButtonReady(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// For speed purposes and better perforamance, we are using useCallback
|
// For speed purposes and better perforamance, we are using useCallback
|
||||||
@@ -422,7 +394,7 @@ export default function Dashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Once "Save changes" is clicked, disable that button
|
// Once "Save changes" is clicked, disable that button
|
||||||
setButtonReady(false);
|
setHasUnsavedChanges(false);
|
||||||
|
|
||||||
const secretsToBeDeleted = initialData!
|
const secretsToBeDeleted = initialData!
|
||||||
.filter(
|
.filter(
|
||||||
@@ -566,7 +538,7 @@ export default function Dashboard() {
|
|||||||
);
|
);
|
||||||
return filteredOldData.concat(filteredNewData);
|
return filteredOldData.concat(filteredNewData);
|
||||||
});
|
});
|
||||||
setButtonReady(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addData = (newData: SecretDataProps[]) => {
|
const addData = (newData: SecretDataProps[]) => {
|
||||||
@@ -587,6 +559,26 @@ export default function Dashboard() {
|
|||||||
deleteRow({ ids, secretName });
|
deleteRow({ ids, secretName });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnEnvironmentChange = (envName: string) => {
|
||||||
|
if(hasUnsavedChanges) {
|
||||||
|
if (!window.confirm(leaveConfirmDefaultMessage)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedWorkspaceEnv = workspaceEnvs.find(({ name }: { name: string }) => envName === name) || {
|
||||||
|
name: 'unknown',
|
||||||
|
slug: 'unknown',
|
||||||
|
isWriteDenied: false,
|
||||||
|
isReadDenied: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (selectedWorkspaceEnv) {
|
||||||
|
if (snapshotData) setSelectedSnapshotEnv(selectedWorkspaceEnv);
|
||||||
|
else setSelectedEnv(selectedWorkspaceEnv);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
|
};
|
||||||
|
|
||||||
return data ? (
|
return data ? (
|
||||||
<div className="bg-bunker-800 max-h-screen h-full relative flex flex-col justify-between text-white dark">
|
<div className="bg-bunker-800 max-h-screen h-full relative flex flex-col justify-between text-white dark">
|
||||||
<Head>
|
<Head>
|
||||||
@@ -655,16 +647,7 @@ export default function Dashboard() {
|
|||||||
<ListBox
|
<ListBox
|
||||||
isSelected={selectedEnv.name}
|
isSelected={selectedEnv.name}
|
||||||
data={workspaceEnvs.map(({ name }) => name)}
|
data={workspaceEnvs.map(({ name }) => name)}
|
||||||
onChange={(envName) =>
|
onChange={handleOnEnvironmentChange}
|
||||||
setSelectedEnv(
|
|
||||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
|
||||||
name: 'unknown',
|
|
||||||
slug: 'unknown',
|
|
||||||
isWriteDenied: false,
|
|
||||||
isReadDenied: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -681,14 +664,14 @@ export default function Dashboard() {
|
|||||||
icon={faClockRotateLeft}
|
icon={faClockRotateLeft}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
{(data?.length !== 0 || buttonReady) && !snapshotData && (
|
{(data?.length !== 0 || hasUnsavedChanges) && !snapshotData && (
|
||||||
<div className="flex justify-start max-w-sm mt-1">
|
<div className="flex justify-start max-w-sm mt-1">
|
||||||
<Button
|
<Button
|
||||||
text={String(t('common:save-changes'))}
|
text={String(t('common:save-changes'))}
|
||||||
onButtonPressed={savePush}
|
onButtonPressed={savePush}
|
||||||
color="primary"
|
color="primary"
|
||||||
size="md"
|
size="md"
|
||||||
active={buttonReady}
|
active={hasUnsavedChanges}
|
||||||
iconDisabled={faCheck}
|
iconDisabled={faCheck}
|
||||||
textDisabled={String(t('common:saved'))}
|
textDisabled={String(t('common:saved'))}
|
||||||
loading={saveLoading}
|
loading={saveLoading}
|
||||||
@@ -723,11 +706,11 @@ export default function Dashboard() {
|
|||||||
text: `Rollback has been performed successfully.`,
|
text: `Rollback has been performed successfully.`,
|
||||||
type: 'success'
|
type: 'success'
|
||||||
});
|
});
|
||||||
setButtonReady(false);
|
setHasUnsavedChanges(false);
|
||||||
}}
|
}}
|
||||||
color="primary"
|
color="primary"
|
||||||
size="md"
|
size="md"
|
||||||
active={buttonReady}
|
active={hasUnsavedChanges}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -742,31 +725,13 @@ export default function Dashboard() {
|
|||||||
<ListBox
|
<ListBox
|
||||||
isSelected={selectedEnv.name}
|
isSelected={selectedEnv.name}
|
||||||
data={workspaceEnvs.map(({ name }) => name)}
|
data={workspaceEnvs.map(({ name }) => name)}
|
||||||
onChange={(envName) =>
|
onChange={handleOnEnvironmentChange}
|
||||||
setSelectedEnv(
|
|
||||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
|
||||||
name: 'unknown',
|
|
||||||
slug: 'unknown',
|
|
||||||
isWriteDenied: false,
|
|
||||||
isReadDenied: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ListBox
|
<ListBox
|
||||||
isSelected={selectedSnapshotEnv?.name || ''}
|
isSelected={selectedSnapshotEnv?.name || ''}
|
||||||
data={workspaceEnvs.map(({ name }) => name)}
|
data={workspaceEnvs.map(({ name }) => name)}
|
||||||
onChange={(envName) =>
|
onChange={handleOnEnvironmentChange}
|
||||||
setSelectedSnapshotEnv(
|
|
||||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
|
||||||
name: 'unknown',
|
|
||||||
slug: 'unknown',
|
|
||||||
isWriteDenied: false,
|
|
||||||
isReadDenied: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="h-10 w-full bg-mineshaft-700 hover:bg-white/10 ml-2 rounded-md flex flex-row items-center">
|
<div className="h-10 w-full bg-mineshaft-700 hover:bg-white/10 ml-2 rounded-md flex flex-row items-center">
|
||||||
@@ -970,7 +935,7 @@ export default function Dashboard() {
|
|||||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||||
createNewFile={addRow}
|
createNewFile={addRow}
|
||||||
errorDragAndDrop={errorDragAndDrop}
|
errorDragAndDrop={errorDragAndDrop}
|
||||||
setButtonReady={setButtonReady}
|
setButtonReady={setHasUnsavedChanges}
|
||||||
keysExist
|
keysExist
|
||||||
numCurrentRows={data.length}
|
numCurrentRows={data.length}
|
||||||
/>
|
/>
|
||||||
@@ -986,7 +951,7 @@ export default function Dashboard() {
|
|||||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||||
createNewFile={addRow}
|
createNewFile={addRow}
|
||||||
errorDragAndDrop={errorDragAndDrop}
|
errorDragAndDrop={errorDragAndDrop}
|
||||||
setButtonReady={setButtonReady}
|
setButtonReady={setHasUnsavedChanges}
|
||||||
numCurrentRows={data.length}
|
numCurrentRows={data.length}
|
||||||
keysExist={false}
|
keysExist={false}
|
||||||
/>
|
/>
|
||||||
@@ -1013,7 +978,7 @@ export default function Dashboard() {
|
|||||||
modifyValue={listenChangeValue}
|
modifyValue={listenChangeValue}
|
||||||
modifyValueOverride={listenChangeValueOverride}
|
modifyValueOverride={listenChangeValueOverride}
|
||||||
modifyComment={listenChangeComment}
|
modifyComment={listenChangeComment}
|
||||||
buttonReady={buttonReady}
|
buttonReady={hasUnsavedChanges}
|
||||||
workspaceEnvs={workspaceEnvs}
|
workspaceEnvs={workspaceEnvs}
|
||||||
selectedEnv={selectedEnv!}
|
selectedEnv={selectedEnv!}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
|
|||||||
@@ -198,6 +198,9 @@ export default function Integrations() {
|
|||||||
case 'flyio':
|
case 'flyio':
|
||||||
link = `${window.location.origin}/integrations/flyio/authorize`
|
link = `${window.location.origin}/integrations/flyio/authorize`
|
||||||
break;
|
break;
|
||||||
|
case 'circleci':
|
||||||
|
link = `${window.location.origin}/integrations/circleci/authorize`
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -241,6 +244,9 @@ export default function Integrations() {
|
|||||||
case 'flyio':
|
case 'flyio':
|
||||||
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
|
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
|
||||||
break;
|
break;
|
||||||
|
case 'circleci':
|
||||||
|
link = `${window.location.origin}/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
77
frontend/src/pages/integrations/circleci/authorize.tsx
Normal file
77
frontend/src/pages/integrations/circleci/authorize.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
} from '../../../components/v2';
|
||||||
|
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
|
||||||
|
|
||||||
|
export default function CircleCICreateIntegrationPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [apiKey, setApiKey] = useState('');
|
||||||
|
const [apiKeyErrorText, setApiKeyErrorText] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
setApiKeyErrorText('');
|
||||||
|
if (apiKey.length === 0) {
|
||||||
|
setApiKeyErrorText('API Key cannot be blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const integrationAuth = await saveIntegrationAccessToken({
|
||||||
|
workspaceId: localStorage.getItem('projectData.id'),
|
||||||
|
integration: 'circleci',
|
||||||
|
accessToken: apiKey,
|
||||||
|
accessId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
router.push(
|
||||||
|
`/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full flex justify-center items-center">
|
||||||
|
<Card className="max-w-md p-8 rounded-md">
|
||||||
|
<CardTitle className='text-center'>CircleCI Integration</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="CircleCI API Key"
|
||||||
|
errorText={apiKeyErrorText}
|
||||||
|
isError={apiKeyErrorText !== '' ?? false}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder=''
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
className='mt-4'
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to CircleCI
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CircleCICreateIntegrationPage.requireAuth = true;
|
||||||
|
|
||||||
|
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
|
||||||
124
frontend/src/pages/integrations/circleci/create.tsx
Normal file
124
frontend/src/pages/integrations/circleci/create.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import queryString from 'query-string';
|
||||||
|
|
||||||
|
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from '../../../components/v2';
|
||||||
|
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
|
||||||
|
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
|
||||||
|
import createIntegration from "../../api/integrations/createIntegration";
|
||||||
|
|
||||||
|
export default function CircleCICreateIntegrationPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
|
||||||
|
|
||||||
|
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
|
||||||
|
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||||
|
const [targetApp, setTargetApp] = useState('');
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (workspace) {
|
||||||
|
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||||
|
}
|
||||||
|
}, [workspace]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// TODO: handle case where apps can be empty
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
setTargetApp(integrationAuthApps[0]?.name);
|
||||||
|
}
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?._id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await createIntegration({
|
||||||
|
integrationAuthId: integrationAuth?._id,
|
||||||
|
isActive: true,
|
||||||
|
app: targetApp,
|
||||||
|
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
targetEnvironment: null,
|
||||||
|
owner: null,
|
||||||
|
path: null,
|
||||||
|
region: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
router.push(
|
||||||
|
`/integrations/${localStorage.getItem('projectData.id')}`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
|
||||||
|
<div className="h-full w-full flex justify-center items-center">
|
||||||
|
<Card className="max-w-md p-8 rounded-md">
|
||||||
|
<CardTitle className='text-center'>CircleCI Integration</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="Project Environment"
|
||||||
|
className='mt-4'
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={selectedSourceEnvironment}
|
||||||
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
|
className='w-full border border-mineshaft-500'
|
||||||
|
>
|
||||||
|
{workspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
label="CircleCI Service"
|
||||||
|
className='mt-4'
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={targetApp}
|
||||||
|
onValueChange={(val) => setTargetApp(val)}
|
||||||
|
className='w-full border border-mineshaft-500'
|
||||||
|
>
|
||||||
|
{integrationAuthApps.map((integrationAuthApp) => (
|
||||||
|
<SelectItem value={integrationAuthApp.name} key={`render-environment-${integrationAuthApp.name}`}>
|
||||||
|
{integrationAuthApp.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
className='mt-4'
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : <div />
|
||||||
|
}
|
||||||
|
|
||||||
|
CircleCICreateIntegrationPage.requireAuth = true;
|
||||||
|
|
||||||
|
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
|
||||||
Reference in New Issue
Block a user