Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ test.db
videos/
screenshots/
vrms.code-workspace
.vscode/
.vscode/
.env.test
9 changes: 4 additions & 5 deletions backend/Dockerfile.api
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
FROM node:18.12.0 AS api-development
FROM node:22 AS api-development
RUN mkdir /srv/backend
WORKDIR /srv/backend
RUN mkdir -p node_modules
COPY package.json yarn.lock ./
RUN yarn install --pure-lockfile
COPY . .

FROM node:18.12.0 AS api-test
FROM node:22 AS api-test
RUN mkdir /srv/backend
WORKDIR /srv/backend
COPY package.json yarn.lock ./
RUN yarn install --silent
RUN mkdir -p node_modules

FROM node:18.12.0-slim AS api-production
FROM node:22-slim AS api-production
EXPOSE 4000
USER node
WORKDIR /srv/backend
COPY --from=api-development /srv/backend/node_modules ./node_modules
COPY . .
CMD ["npm", "run", "dev"]

CMD ["npm", "run", "dev"]
2 changes: 2 additions & 0 deletions backend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const projectTeamMembersRouter = require('./routers/projectTeamMembers.router');
//const slackRouter = require("./routers/slack.router");
const authRouter = require('./routers/auth.router');
const healthCheckRouter = require('./routers/healthCheck.router');
const featureFlagsRouter = require('./routers/featureFlags.router');

// Check that clients to the API are sending the custom request header on all methods
// except for ones described in the dontCheckCustomRequestHeaderApis array.
Expand Down Expand Up @@ -122,6 +123,7 @@ app.use('/api/recurringevents', recurringEventsRouter);
app.use('/api/projectteammembers', projectTeamMembersRouter);
//app.use('/api/slack', slackRouter);
app.use('/api/healthcheck', healthCheckRouter);
app.use('/api/featureFlags', featureFlagsRouter);

// 404 for all non-defined endpoints.
app.get('*', (req, res, next) => {
Expand Down
24 changes: 24 additions & 0 deletions backend/controllers/featureFlags.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const { getAllFlags } = require('../services/featureFlags.service.js');

const expectedHeader = process.env.CUSTOM_REQUEST_HEADER;

const FeatureFlagsController = {};

FeatureFlagsController.index = async (req, res) => {
const { headers } = req;

if (headers['x-customrequired-header'] !== expectedHeader) {
return res.sendStatus(403);
}

try {
const distinctId = req.userId || 'anonymous';
const flags = await getAllFlags(distinctId);
return res.status(200).json(flags);
} catch (err) {
console.error(err);
return res.sendStatus(400);
}
};

module.exports = FeatureFlagsController;
36 changes: 36 additions & 0 deletions backend/controllers/featureFlags.controller.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const FeatureFlagsController = require('./featureFlags.controller');

const { getAllFlags } = require('../services/featureFlags.service');

jest.mock('../services/featureFlags.service.js');

describe('FeatureFlagsController.index', () => {
let req, res;

beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
req = {
headers: { 'x-customrequired-header': process.env.CUSTOM_REQUEST_HEADER },
userId: 'user-1',
};
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
sendStatus: jest.fn(),
};
});

test('can get feature flags', async () => {
await FeatureFlagsController.index(req, res);

expect(res.status).toHaveBeenCalledWith(200);
});

test('should return 400 when getAllFlags throws an error', async () => {
getAllFlags.mockRejectedValue(new Error('error'));

await FeatureFlagsController.index(req, res);

expect(res.sendStatus).toHaveBeenCalledWith(400);
});
});
4 changes: 3 additions & 1 deletion backend/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ const UserController = require('./user.controller');
const ProjectController = require('./project.controller');
const HealthCheckController = require('./healthCheck.controller');
const RecurringEventController = require('./recurringEvent.controller')
const FeatureFlagsController = require('./featureFlags.controller');

module.exports = {
EmailController,
EventController,
UserController,
ProjectController,
HealthCheckController,
RecurringEventController
RecurringEventController,
FeatureFlagsController,
};
17 changes: 10 additions & 7 deletions backend/middleware/auth.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,18 @@ function hashToken(token) {
}

function getClientIp(req) {
// Check X-Forwarded-For header (most common)
const forwarded = req.headers['x-forwarded-for'];
if (forwarded) {
// Takes the first IP if there are multiple
return forwarded.split(',')[0].trim();
}

// Check other common headers
return (
req.headers['x-real-ip'] || req.connection.remoteAddress || req.socket.remoteAddress || req.ip
);
}

async function authenticateAccessToken(req, res, next) {
try {
// Extract token from Authorization header
let accessToken =
req.cookies.token || req.headers['x-access-token'] || req.headers['authorization'];

Expand All @@ -60,7 +56,6 @@ async function authenticateAccessToken(req, res, next) {
}

const decoded = jwt.verify(accessToken, SECRET);
// Attach user info to request
req.user = decoded;

next();
Expand All @@ -77,7 +72,6 @@ async function authenticateAccessToken(req, res, next) {
}
}

// shorthand for authenticateAccessToken
const authUser = authenticateAccessToken;

async function authenticateRefreshToken(req, res, next) {
Expand All @@ -104,7 +98,6 @@ async function authenticateRefreshToken(req, res, next) {
return res.status(401).json({ error: 'User not found for this token' });
}

// Attach user & refresh token to request for downstream handlers
req.user = user;
req.refreshToken = tokenDoc;

Expand Down Expand Up @@ -163,6 +156,15 @@ function verifyCookie(req, res, next) {
});
}

function addCookieIfAvailable(req, _res, next) {
jwt.verify(req.cookies.token, SECRET, (err, decoded) => {
if (!err) {
req.userId = decoded.id;
}
next();
});
}

module.exports = {
authenticateAccessToken,
authUser,
Expand All @@ -174,4 +176,5 @@ module.exports = {
getClientIp,
hashToken,
verifyCookie,
addCookieIfAvailable,
};
Loading
Loading