feat(#31): implemented api for environment crud operations

This commit is contained in:
akhilmhdh
2022-12-28 23:42:17 +05:30
parent 861639de27
commit 3ad3e19bcf
15 changed files with 293 additions and 37 deletions

View File

@ -46,6 +46,7 @@ import {
workspace as v2WorkspaceRouter,
serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
} from './routes/v2';
import { healthCheck } from './routes/status';
@ -108,6 +109,7 @@ app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise
app.use('/api/v2/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
app.use('/api/v2/environments', v2EnvironmentRouter);
// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))

View File

@ -31,11 +31,17 @@ export const oAuthExchange = async (
if (!INTEGRATION_SET.has(integration))
throw new Error('Failed to validate integration');
const environments = req.membership.workspace?.environments || [];
if(environments.length === 0){
throw new Error("Failed to get environments")
}
await IntegrationService.handleOAuthExchange({
workspaceId,
integration,
code
code,
environment: environments[0].slug,
});
} catch (err) {
Sentry.setUser(null);

View File

@ -9,7 +9,6 @@ import {
import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../../events';
import { EventService } from '../../services';
import { ENV_SET } from '../../variables';
import { postHogClient } from '../../services';
interface PushSecret {
@ -44,7 +43,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
// validate environment
if (!ENV_SET.has(environment)) {
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
@ -116,7 +116,8 @@ export const pullSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
// validate environment
if (!ENV_SET.has(environment)) {
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
@ -183,7 +184,8 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
// validate environment
if (!ENV_SET.has(environment)) {
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}

View File

@ -1,7 +1,6 @@
import { Request, Response } from 'express';
import { ServiceToken } from '../../models';
import { createToken } from '../../helpers/auth';
import { ENV_SET } from '../../variables';
import { JWT_SERVICE_SECRET } from '../../config';
/**
@ -36,7 +35,8 @@ export const createServiceToken = async (req: Request, res: Response) => {
} = req.body;
// validate environment
if (!ENV_SET.has(environment)) {
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}

View File

@ -0,0 +1,167 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Secret, ServiceToken, Workspace, Integration } from '../../models';
/**
* Create new workspace environment named [environmentName] under workspace with id
* @param req
* @param res
* @returns
*/
export const createWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
const { workspaceId, environmentName, environmentSlug } = req.body;
try {
// atomic create the environment
const workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId,
'environments.slug': { $ne: environmentSlug },
'environments.name': { $ne: environmentName },
},
{
$addToSet: {
environments: { name: environmentName, slug: environmentSlug },
},
}
);
if (!workspace) {
throw new Error('Failed to update workspace environment');
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create new workspace environment',
});
}
return res.status(200).send({
message: 'Successfully created new environment',
workspace: workspaceId,
environment: {
name: environmentName,
slug: environmentSlug,
},
});
};
/**
* Rename workspace environment with new name and slug of a workspace with [workspaceId]
* Old slug [oldEnvironmentSlug] must be provided
* @param req
* @param res
* @returns
*/
export const renameWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
const { workspaceId, environmentName, environmentSlug, oldEnvironmentSlug } =
req.body;
try {
// user should pass both new slug and env name
if (!environmentSlug || !environmentName) {
throw new Error('Invalid environment given.');
}
// atomic update the env to avoid conflict
const workspace = await Workspace.findOneAndUpdate(
{ _id: workspaceId, 'environments.slug': oldEnvironmentSlug },
{
'environments.$.name': environmentName,
'environments.$.slug': environmentSlug,
}
);
if (!workspace) {
throw new Error('Failed to update workspace');
}
await Secret.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceToken.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update workspace environment',
});
}
return res.status(200).send({
message: 'Successfully update environment',
workspace: workspaceId,
environment: {
name: environmentName,
slug: environmentSlug,
},
});
};
/**
* Delete workspace environment by [environmentSlug] of workspace [workspaceId] and do the clean up
* @param req
* @param res
* @returns
*/
export const deleteWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
const { workspaceId, environmentSlug } = req.body;
try {
// atomic delete the env in the workspacce
const workspace = await Workspace.findOneAndUpdate(
{ _id: workspaceId },
{
$pull: {
environments: {
slug: environmentSlug,
},
},
}
);
if (!workspace) {
throw new Error('Failed to delete workspace environment');
}
// clean up
await Secret.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceToken.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace environment',
});
}
return res.status(200).send({
message: 'Successfully deleted environment',
workspace: workspaceId,
environment: environmentSlug,
});
};

View File

@ -3,11 +3,13 @@ import * as serviceTokenDataController from './serviceTokenDataController';
import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController';
import * as secretsController from './secretsController';
import * as environmentController from './environmentController';
export {
workspaceController,
serviceTokenDataController,
apiKeyDataController,
secretController,
secretsController
secretsController,
environmentController
}

View File

@ -19,7 +19,6 @@ import {
import { pushKeys } from '../../helpers/key';
import { postHogClient, EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { ENV_SET } from '../../variables';
interface V2PushSecret {
type: string; // personal or shared
@ -52,7 +51,8 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
// validate environment
if (!ENV_SET.has(environment)) {
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
@ -129,6 +129,11 @@ export const pullSecrets = async (req: Request, res: Response) => {
} else if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
}
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
secrets = await pull({
userId,

View File

@ -7,8 +7,6 @@ import {
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService } from '../services';
import {
ENV_DEV,
EVENT_PUSH_SECRETS,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
} from '../variables';
@ -36,11 +34,13 @@ interface Update {
const handleOAuthExchangeHelper = async ({
workspaceId,
integration,
code
code,
environment
}: {
workspaceId: string;
integration: string;
code: string;
environment: string;
}) => {
let action;
let integrationAuth;
@ -102,9 +102,9 @@ const handleOAuthExchangeHelper = async ({
// initialize new integration after exchange
await new Integration({
workspace: workspaceId,
environment: ENV_DEV,
isActive: false,
app: null,
environment,
integration,
integrationAuth: integrationAuth._id
}).save();

View File

@ -1,9 +1,5 @@
import { Schema, model, Types } from 'mongoose';
import {
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@ -32,7 +28,6 @@ const integrationSchema = new Schema<IIntegration>(
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
isActive: {

View File

@ -2,10 +2,6 @@ import { Schema, model, Types } from 'mongoose';
import {
SECRET_SHARED,
SECRET_PERSONAL,
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD
} from '../variables';
export interface ISecret {
@ -53,7 +49,6 @@ const secretSchema = new Schema<ISecret>(
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
secretKeyCiphertext: {

View File

@ -1,7 +1,4 @@
import { Schema, model, Types } from 'mongoose';
import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables';
// TODO: deprecate
export interface IServiceToken {
_id: Types.ObjectId;
name: string;
@ -33,7 +30,6 @@ const serviceTokenSchema = new Schema<IServiceToken>(
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
expiresAt: {

View File

@ -4,6 +4,10 @@ export interface IWorkspace {
_id: Types.ObjectId;
name: string;
organization: Types.ObjectId;
environments: Array<{
name: string;
slug: string;
}>;
}
const workspaceSchema = new Schema<IWorkspace>({
@ -15,7 +19,33 @@ const workspaceSchema = new Schema<IWorkspace>({
type: Schema.Types.ObjectId,
ref: 'Organization',
required: true
}
},
environments: {
type: [
{
name: String,
slug: String,
},
],
default: [
{
name: "development",
slug: "dev"
},
{
name: "test",
slug: "test"
},
{
name: "staging",
slug: "staging"
},
{
name: "production",
slug: "prod"
}
],
},
});
const Workspace = model<IWorkspace>('Workspace', workspaceSchema);

View File

@ -0,0 +1,54 @@
import express from 'express';
const router = express.Router();
import { body, param } from 'express-validator';
import { environmentController } from '../../controllers/v2';
import {
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from '../../middleware';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
router.post(
'/:workspaceId',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED],
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
body('environmentName').exists().trim(),
validateRequest,
environmentController.createWorkspaceEnvironment
);
router.put(
'/:workspaceId',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED],
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
body('environmentName').exists().trim(),
body('oldEnvironmentSlug').exists().trim(),
validateRequest,
environmentController.renameWorkspaceEnvironment
);
router.delete(
'/:workspaceId',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
acceptedStatuses: [GRANTED],
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
validateRequest,
environmentController.deleteWorkspaceEnvironment
);
export default router;

View File

@ -3,11 +3,13 @@ import secrets from './secrets';
import workspace from './workspace';
import serviceTokenData from './serviceTokenData';
import apiKeyData from './apiKeyData';
import environment from "./environment"
export {
secret,
secrets,
workspace,
serviceTokenData,
apiKeyData
}
apiKeyData,
environment
}

View File

@ -11,10 +11,6 @@ import {
setIntegrationAuthAccessHelper,
} from '../helpers/integration';
import { exchangeCode } from '../integrations';
import {
ENV_DEV,
EVENT_PUSH_SECRETS
} from '../variables';
// should sync stuff be here too? Probably.
// TODO: move bot functions to IntegrationService.
@ -32,22 +28,26 @@ class IntegrationService {
* - Create bot sequence for integration
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.environment - workspace environment
* @param {String} obj.integration - name of integration
* @param {String} obj.code - code
*/
static async handleOAuthExchange({
workspaceId,
integration,
code
code,
environment
}: {
workspaceId: string;
integration: string;
code: string;
environment: string;
}) {
await handleOAuthExchangeHelper({
workspaceId,
integration,
code
code,
environment
});
}