Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add: Feature flag service backend integration #2266

Closed
wants to merge 8 commits into from
Closed
4 changes: 4 additions & 0 deletions config/custom-environment-variables.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ module.exports = {
discordBot: {
baseUrl: "DISCORD_BASE_URL",
},
featureFlag: {
baseUrl: "FEATURE_FLAG_SERVICE_BASE_URL",
apiKey: "FEATURE_FLAG_SERVICE_API_KEY",
},
},

emailServiceConfig: {
Expand Down
4 changes: 4 additions & 0 deletions config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,14 @@
discordBot: {
baseUrl: "<DISCORD_BOT_BASE_URL>",
},
featureFlag: {
baseUrl: "<FEATURE_FLAG_SERVICE_BASE_URL>",
apiKey: "<FEATURE_FLAG_SERVICE_API_KEY>",
},
},

cors: {
allowedOrigins: /(https:\/\/([a-zA-Z0-9-_]+\.)?realdevsquad\.com$)/, // Allow realdevsquad.com, *.realdevsquad.com

Check warning on line 80 in config/default.js

View workflow job for this annotation

GitHub Actions / build (20.11.x)

Unsafe Regular Expression
},

userToken: {
Expand Down
73 changes: 73 additions & 0 deletions controllers/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { CustomRequest, CustomResponse } from "../types/global";
import featureFlagService from "../services/featureFlagService";
import { FeatureFlag, UpdateFeatureFlagRequestBody } from "../types/featureFlags";

export const getAllFeatureFlags = async (req: CustomRequest, res: CustomResponse) => {
try {
const serviceResponse = await featureFlagService.getAllFeatureFlags();

if (serviceResponse.error) {
return res.status(serviceResponse.status).json({
error: serviceResponse.error.message
});
}

return res.status(200).json({
message: "Feature flags retrieved successfully",
data: serviceResponse
});

} catch (err) {
logger.error(`Error in fetching feature flags: ${err}`);
return res.boom.badImplementation('Internal server error');
}
};


export const getFeatureFlagById = async (req: CustomRequest, res: CustomResponse) => {
try {
const { flagId } = req.params;
const serviceResponse = await featureFlagService.getFeatureFlagById(flagId);

if (serviceResponse.data) {
return res.status(serviceResponse.status).json({
message: "Feature flag retrieved successfully",
data: serviceResponse.data,
});
} else if (serviceResponse.error) {
return res.status(serviceResponse.status).json({
error: serviceResponse.error.message || "Internal server error",
});
} else {
return res.status(500).json({ error: "Unknown error occurred" });
}
} catch (err) {
logger.error(`Unexpected error in fetching feature flag: ${err}`);
return res.boom.badImplementation("Internal server error");
}
};

export const createFeatureFlag = async (req: CustomRequest, res: CustomResponse) => {
try {
const flagData: Partial<FeatureFlag> = req.body as Partial<FeatureFlag>;

const serviceResponse = await featureFlagService.createFeatureFlag(flagData);

if (serviceResponse.data) {
return res.status(serviceResponse.status).json({
message: "Feature flag created successfully",
data: serviceResponse.data,
});
} else if (serviceResponse.error) {

return res.status(serviceResponse.status).json({
error: serviceResponse.error.message || "Internal server error",
});
} else {
return res.status(500).json({ error: "Unknown error occurred" });
}
} catch (err) {
logger.error(`Unexpected error in creating feature flag: ${err}`);
return res.boom.badImplementation("Internal server error.");
}
};
31 changes: 31 additions & 0 deletions middlewares/validators/featureFlag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Joi from 'joi';
import { Request, Response, NextFunction } from 'express';
import { CustomResponse } from '../../types/global';

const createFeatureFlagSchema = Joi.object({
Name: Joi.string()
.required()
.messages({
'any.required': 'Name is required'
}),
Description: Joi.string()
.required()
.messages({
'any.required': 'Description is required'
}),
UserId: Joi.string()
.required()
.messages({
'any.required': 'UserId is required'
})
});

export const validateCreateFeatureFlag = async (req: Request, res: CustomResponse, next: NextFunction) => {
try {
await createFeatureFlagSchema.validateAsync(req.body);
next();
} catch (error) {
logger.error(`Error validating create feature flag payload: ${error.message}`);
res.boom.badRequest(error.message);
}
};
13 changes: 13 additions & 0 deletions routes/featureFlag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import express from "express";
const router = express.Router();
import authenticate from "../middlewares/authenticate";
const authorizeRoles = require("../middlewares/authorizeRoles");
import { createFeatureFlag, getAllFeatureFlags, getFeatureFlagById } from "../controllers/featureFlags";
const { SUPERUSER } = require("../constants/roles");
import { validateCreateFeatureFlag } from '../middlewares/validators/featureFlag';

router.get("/getAllFeatureFlags", authenticate, getAllFeatureFlags);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix AI about 1 month ago

To fix the problem, we should introduce rate limiting to the routes in the routes/featureFlag.ts file. The best way to do this is by using the express-rate-limit package, which allows us to easily set up rate limiting for our Express routes.

  1. Install the express-rate-limit package.
  2. Import the express-rate-limit package in the routes/featureFlag.ts file.
  3. Set up a rate limiter with appropriate configuration (e.g., maximum of 100 requests per 15 minutes).
  4. Apply the rate limiter to the routes that need protection.
Suggested changeset 2
routes/featureFlag.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/routes/featureFlag.ts b/routes/featureFlag.ts
--- a/routes/featureFlag.ts
+++ b/routes/featureFlag.ts
@@ -7,6 +7,12 @@
 import { validateCreateFeatureFlag } from '../middlewares/validators/featureFlag';
+import rateLimit from 'express-rate-limit';
 
-router.get("/getAllFeatureFlags", authenticate, getAllFeatureFlags);
-router.get("/getFeatureFlag/:flagId", authenticate, getFeatureFlagById);
-router.post('/createFeatureFlag', authenticate, authorizeRoles([SUPERUSER]), validateCreateFeatureFlag, createFeatureFlag);
+const limiter = rateLimit({
+  windowMs: 15 * 60 * 1000, // 15 minutes
+  max: 100, // max 100 requests per windowMs
+});
+
+router.get("/getAllFeatureFlags", authenticate, limiter, getAllFeatureFlags);
+router.get("/getFeatureFlag/:flagId", authenticate, limiter, getFeatureFlagById);
+router.post('/createFeatureFlag', authenticate, authorizeRoles([SUPERUSER]), validateCreateFeatureFlag, limiter, createFeatureFlag);
 
EOF
@@ -7,6 +7,12 @@
import { validateCreateFeatureFlag } from '../middlewares/validators/featureFlag';
import rateLimit from 'express-rate-limit';

router.get("/getAllFeatureFlags", authenticate, getAllFeatureFlags);
router.get("/getFeatureFlag/:flagId", authenticate, getFeatureFlagById);
router.post('/createFeatureFlag', authenticate, authorizeRoles([SUPERUSER]), validateCreateFeatureFlag, createFeatureFlag);
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // max 100 requests per windowMs
});

router.get("/getAllFeatureFlags", authenticate, limiter, getAllFeatureFlags);
router.get("/getFeatureFlag/:flagId", authenticate, limiter, getFeatureFlagById);
router.post('/createFeatureFlag', authenticate, authorizeRoles([SUPERUSER]), validateCreateFeatureFlag, limiter, createFeatureFlag);

package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -43,3 +43,4 @@
     "rate-limiter-flexible": "5.0.3",
-    "winston": "3.13.0"
+    "winston": "3.13.0",
+    "express-rate-limit": "^7.4.1"
   },
EOF
@@ -43,3 +43,4 @@
"rate-limiter-flexible": "5.0.3",
"winston": "3.13.0"
"winston": "3.13.0",
"express-rate-limit": "^7.4.1"
},
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 7.4.1 None
Copilot is powered by AI and may make mistakes. Always verify output.
Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
router.get("/getFeatureFlag/:flagId", authenticate, getFeatureFlagById);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix AI about 1 month ago

To fix the problem, we need to introduce rate limiting to the route handler in question. The best way to do this is by using the express-rate-limit package, which provides a simple and effective way to limit the number of requests a client can make to the server within a specified time window.

  1. Install the express-rate-limit package.
  2. Import the express-rate-limit package in the routes/featureFlag.ts file.
  3. Set up a rate limiter with appropriate configuration (e.g., maximum of 100 requests per 15 minutes).
  4. Apply the rate limiter to the route handler on line 10.
Suggested changeset 2
routes/featureFlag.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/routes/featureFlag.ts b/routes/featureFlag.ts
--- a/routes/featureFlag.ts
+++ b/routes/featureFlag.ts
@@ -7,5 +7,11 @@
 import { validateCreateFeatureFlag } from '../middlewares/validators/featureFlag';
+import rateLimit from 'express-rate-limit';
+
+const limiter = rateLimit({
+  windowMs: 15 * 60 * 1000, // 15 minutes
+  max: 100, // limit each IP to 100 requests per windowMs
+});
 
 router.get("/getAllFeatureFlags", authenticate, getAllFeatureFlags);
-router.get("/getFeatureFlag/:flagId", authenticate, getFeatureFlagById);
+router.get("/getFeatureFlag/:flagId", authenticate, limiter, getFeatureFlagById);
 router.post('/createFeatureFlag', authenticate, authorizeRoles([SUPERUSER]), validateCreateFeatureFlag, createFeatureFlag);
EOF
@@ -7,5 +7,11 @@
import { validateCreateFeatureFlag } from '../middlewares/validators/featureFlag';
import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
});

router.get("/getAllFeatureFlags", authenticate, getAllFeatureFlags);
router.get("/getFeatureFlag/:flagId", authenticate, getFeatureFlagById);
router.get("/getFeatureFlag/:flagId", authenticate, limiter, getFeatureFlagById);
router.post('/createFeatureFlag', authenticate, authorizeRoles([SUPERUSER]), validateCreateFeatureFlag, createFeatureFlag);
package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -43,3 +43,4 @@
     "rate-limiter-flexible": "5.0.3",
-    "winston": "3.13.0"
+    "winston": "3.13.0",
+    "express-rate-limit": "^7.4.1"
   },
EOF
@@ -43,3 +43,4 @@
"rate-limiter-flexible": "5.0.3",
"winston": "3.13.0"
"winston": "3.13.0",
"express-rate-limit": "^7.4.1"
},
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 7.4.1 None
Copilot is powered by AI and may make mistakes. Always verify output.
Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
router.post('/createFeatureFlag', authenticate, authorizeRoles([SUPERUSER]), validateCreateFeatureFlag, createFeatureFlag);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix AI about 1 month ago

To fix the problem, we need to introduce rate limiting to the route handler to prevent potential denial-of-service attacks. The best way to do this is by using the express-rate-limit package, which allows us to set up rate limiting middleware easily.

We will:

  1. Install the express-rate-limit package.
  2. Import the package in the routes/featureFlag.ts file.
  3. Set up a rate limiter with appropriate configuration.
  4. Apply the rate limiter to the route handler.
Suggested changeset 2
routes/featureFlag.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/routes/featureFlag.ts b/routes/featureFlag.ts
--- a/routes/featureFlag.ts
+++ b/routes/featureFlag.ts
@@ -7,2 +7,8 @@
 import { validateCreateFeatureFlag } from '../middlewares/validators/featureFlag';
+import rateLimit from 'express-rate-limit';
+
+const limiter = rateLimit({
+  windowMs: 15 * 60 * 1000, // 15 minutes
+  max: 100, // limit each IP to 100 requests per windowMs
+});
 
@@ -10,3 +16,3 @@
 router.get("/getFeatureFlag/:flagId", authenticate, getFeatureFlagById);
-router.post('/createFeatureFlag', authenticate, authorizeRoles([SUPERUSER]), validateCreateFeatureFlag, createFeatureFlag);
+router.post('/createFeatureFlag', limiter, authenticate, authorizeRoles([SUPERUSER]), validateCreateFeatureFlag, createFeatureFlag);
 
EOF
@@ -7,2 +7,8 @@
import { validateCreateFeatureFlag } from '../middlewares/validators/featureFlag';
import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
});

@@ -10,3 +16,3 @@
router.get("/getFeatureFlag/:flagId", authenticate, getFeatureFlagById);
router.post('/createFeatureFlag', authenticate, authorizeRoles([SUPERUSER]), validateCreateFeatureFlag, createFeatureFlag);
router.post('/createFeatureFlag', limiter, authenticate, authorizeRoles([SUPERUSER]), validateCreateFeatureFlag, createFeatureFlag);

package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -43,3 +43,4 @@
     "rate-limiter-flexible": "5.0.3",
-    "winston": "3.13.0"
+    "winston": "3.13.0",
+    "express-rate-limit": "^7.4.1"
   },
EOF
@@ -43,3 +43,4 @@
"rate-limiter-flexible": "5.0.3",
"winston": "3.13.0"
"winston": "3.13.0",
"express-rate-limit": "^7.4.1"
},
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 7.4.1 None
Copilot is powered by AI and may make mistakes. Always verify output.
Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options

module.exports = router;
2 changes: 2 additions & 0 deletions routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@ app.use("/goals", require("./goals"));
app.use("/invites", require("./invites"));
app.use("/requests", require("./requests"));
app.use("/subscription", devFlagMiddleware, require("./subscription"));
app.use("/feature-flag", require("./featureFlag"));

module.exports = app;
103 changes: 103 additions & 0 deletions services/featureFlagService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import config from "config";
import { FeatureFlag, FeatureFlagResponse, FeatureFlagService } from "../types/featureFlags";

const FEATURE_FLAG_BASE_URL = config.get<string>("services.featureFlag.baseUrl");
const FEATURE_FLAG_API_KEY = config.get<string>("services.featureFlag.apiKey");

const defaultHeaders: HeadersInit = {
"Content-Type": "application/json",
"x-api-key": FEATURE_FLAG_API_KEY,
};

const getAllFeatureFlags = async (): Promise<FeatureFlagResponse> => {
try {
const response = await fetch(`${FEATURE_FLAG_BASE_URL}/feature-flags`, {
method: "GET",
headers: defaultHeaders,
});

if (!response.ok) {
logger.error(`Failed to fetch feature flags. Status: ${response.status}`);
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
return data;
} catch (err) {
logger.error("Error in fetching feature flags", err);
return { status: 500, error: { message: "Internal error while connecting to the feature flag service" } };
}
};

const createFeatureFlag = async (flagData: any): Promise<{ status: number; data?: any; error?: any }> => {
try {
const response = await fetch(`${FEATURE_FLAG_BASE_URL}/feature-flags`, {
method: "POST",
headers: defaultHeaders,
body: JSON.stringify(flagData),
});
const status = response.status;

if (response.ok) {
const data = await response.json().catch(() => ({
message: "Feature flag created successfully",
}));
return { status, data };
} else {
const error = await response.json().catch(() => ({
message: "An error occurred while creating the feature flag",
}));
return { status, error };
}
} catch (err) {
logger.error("Error in createFeatureFlag service:", err);
return { status: 500, error: { message: "Internal error while connecting to the feature flag service" } };
}
};

const getFeatureFlagById = async (flagId: string): Promise<{ status: number; data?: any; error?: any }> => {
try {
const response = await fetch(`${FEATURE_FLAG_BASE_URL}/feature-flags/${flagId}`, {
method: "GET",
headers: defaultHeaders,
});
Comment on lines +60 to +63

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.
const status = response.status;
const responseText = await response.text();

if (response.ok) {
try {
const parsedData = JSON.parse(responseText);
return {
status,
data: parsedData
};
} catch (parseError) {
logger.error("Error parsing success response:", parseError);
return {
status: 500,
error: { message: "Error parsing service response" }
};
}
}
return {
status,
error: { message: responseText }
};
} catch (err) {
logger.error("Error in getFeatureFlagById service:", err);
return {
status: 500,
error: {
message: err instanceof Error ? err.message : "Internal error while connecting to the feature flag service"
}
};
}
};

const featureFlagService: FeatureFlagService = {
getAllFeatureFlags,
createFeatureFlag,
getFeatureFlagById,
};

export default featureFlagService;
4 changes: 4 additions & 0 deletions test/config/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ module.exports = {
secretKey: "<goalSecretKey>",
cookieName: `goals-session-test`,
},
featureFlag: {
baseUrl: "FEATURE_FLAG_SERVICE_BASE_URL",
apiKey: "FEATURE_FLAG_SERVICE_API_KEY",
},
},

cors: {
Expand Down
60 changes: 60 additions & 0 deletions test/fixtures/featureFlag/featureFlag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
interface FeatureFlag {
id: string;
name: string;
description: string;
status: string;
createdAt: number;
createdBy: string;
updatedAt: number;
updatedBy: string;
}

const featureFlagData = [
{
id: "60b00c3a-3928-4f0c-8581-dfce71aa8605",
name: "feature-flag",
description: "It is a demo project",
status: "ENABLED",
createdAt: 1718139019,
createdBy: "sduhasdjasdas",
updatedAt: 1718139019,
updatedBy: "sduhasdjasdas"
},
{
id: "flag-1",
name: "feature-flag-1",
description: "First demo flag",
status: "ENABLED",
createdAt: 1718139019,
createdBy: "user1",
updatedAt: 1718139019,
updatedBy: "user1"
},
{
id: "flag-2",
name: "feature-flag-2",
description: "Second demo flag",
status: "DISABLED",
createdAt: 1718139019,
createdBy: "user2",
updatedAt: 1718139019,
updatedBy: "user2"
}
];

const newFeatureFlag = {
Name: "Demo-feature",
Description: "Description for demo feature",
UserId: "superUserId"
};

const invalidFeatureFlag = {
Description: "Missing required name",
UserId: "superUserId"
};

module.exports = {
featureFlagData,
newFeatureFlag,
invalidFeatureFlag
};
29 changes: 29 additions & 0 deletions types/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export interface FeatureFlag {
id?: string;
name: string;
description?: string;
status: string;
createdAt?: number;
updatedAt?: number;
createdBy: string
updatedBy: string
}

export interface FeatureFlagResponse {
status: number;
data?: FeatureFlag | FeatureFlag[];
error?: {
message: string;
};
}

export interface FeatureFlagService {
getAllFeatureFlags(): Promise<FeatureFlagResponse>;
createFeatureFlag(flagData: Partial<FeatureFlag>): Promise<FeatureFlagResponse>;
getFeatureFlagById: (flagId: string) => Promise<FeatureFlagResponse>;
}

export interface UpdateFeatureFlagRequestBody {
Status: string;
UserId: string;
}
Loading