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
16 changes: 10 additions & 6 deletions backend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

// Load in all of our node modules. Their uses are explained below as they are called.
const express = require('express');
const cron = require('node-cron');
const fetch = require('node-fetch');
// const cron = require('node-cron');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these commented out?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't being used, so the build rejected it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If thery are not needed, they should be removed not commented out.

// const fetch = require('node-fetch');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');

Expand Down Expand Up @@ -52,14 +52,18 @@ app.use(cookieParser());
app.use(morgan('dev'));

// WORKERS
const runOpenCheckinWorker = require('./workers/openCheckins')(cron, fetch);
const runCloseCheckinWorker = require('./workers/closeCheckins')(cron, fetch);
// const runOpenCheckinWorker = require('./workers/openCheckins')(cron, fetch);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these commected out?

Copy link
Member

@geolunalg geolunalg Mar 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here if we don't need these they should be removed.

// const runCloseCheckinWorker = require('./workers/closeCheckins')(cron, fetch);

const { createRecurringEvents } = require('./workers/createRecurringEvents');
const runCreateRecurringEventsWorker = createRecurringEvents(cron, fetch);
// const { createRecurringEvents } = require('./workers/createRecurringEvents');
// const runCreateRecurringEventsWorker = createRecurringEvents(cron, fetch);

// const runSlackBot = require("./workers/slackbot")(fetch);

// Run cleanup expired refresh token(s) on startup
const { cleanupExpiredTokens } = require('./workers/tokenCleanup');
cleanupExpiredTokens();

// MIDDLEWARE
const errorhandler = require('./middleware/errorhandler.middleware');

Expand Down
10 changes: 5 additions & 5 deletions backend/config/auth.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/*eslint-disable */
module.exports = {
SECRET:
'c0d7d0716e4cecffe9dcc77ff90476d98f5aace08ea40f5516bd982b06401021191f0f24cd6759f7d8ca41b64f68d0b3ad19417453bddfd1dbe8fcb197245079',
TOKEN_SECRET: process.env.TOKEN_SECRET || 'placeholder_secret_key_for_development_only',
CUSTOM_REQUEST_HEADER: process.env.CUSTOM_REQUEST_HEADER,
TOKEN_EXPIRATION_SEC: 900,
// 15 minutes as a string for JWT expiration
ACCESS_TOKEN_EXPIRATION: '15m',
// 30 days in milliseconds for refresh token expiration
REFRESH_TOKEN_EXPIRATION_MS: 30 * 24 * 60 * 60 * 1000,
};
/* eslint-enable */
65 changes: 48 additions & 17 deletions backend/controllers/user.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ const { ObjectId } = require('mongodb');
const EmailController = require('./email.controller');
const { CONFIG_AUTH } = require('../config');

const { User, Project } = require('../models');
const { User, Project, RefreshToken } = require('../models');
const {
generateRefreshToken,
getClientIp,
hashToken,
generateAccessToken,
} = require('../middleware/auth.middleware');

const expectedHeader = process.env.CUSTOM_REQUEST_HEADER;

Expand Down Expand Up @@ -193,17 +199,6 @@ UserController.delete = async function (req, res) {
}
};

function generateAccessToken(user, auth_origin) {
// expires after half and hour (1800 seconds = 30 minutes)
return jwt.sign(
{ id: user.id, role: user.accessLevel, auth_origin: auth_origin },
CONFIG_AUTH.SECRET,
{
expiresIn: `${CONFIG_AUTH.TOKEN_EXPIRATION_SEC}s`,
},
);
}

UserController.createUser = function (req, res) {
const { firstName, lastName, email } = req.body;
const { origin } = req.headers;
Expand Down Expand Up @@ -267,21 +262,57 @@ UserController.verifySignIn = async function (req, res) {
try {
const payload = jwt.verify(token, CONFIG_AUTH.SECRET);
const user = await User.findById(payload.id);
res.cookie('token', token, { httpOnly: true });
return res.send(user);
const refreshToken = generateRefreshToken();
const accessToken = generateAccessToken(user, payload.auth_origin);
const ipAddress = getClientIp(req);

await RefreshToken.create({
userId: user._id,
hash: hashToken(refreshToken),
deviceInfo: {
deviceType: req.headers['user-agent'],
ipAddress: ipAddress,
},
});

res.cookie('token', accessToken, { httpOnly: true });
res.cookie('refresh_token', refreshToken, { httpOnly: true });

return res.send({
user: user,
expiresAt: accessToken.exp * 1000, // Convert JWT exp (seconds) to milliseconds
});
} catch (err) {
console.error(err);
return res.status(403);
}
};

UserController.verifyMe = async function (req, res) {
const user = await User.findById(req.userId);
return res.status(200).send(user);
return res.status(200).send(req.user);
};

UserController.logout = async function (req, res) {
return res.clearCookie('token').status(200).send('Successfully logged out.');
try {
await RefreshToken.deleteOne({ _id: req.refreshToken._id });
return res.clearCookie('token').status(200).send('Successfully logged out.');
} catch (err) {
console.error(err);
return res.status(500).send('Error occurred while logging out.');
}
};

UserController.refreshAccessToken = async function (req, res) {
const accessToken = generateAccessToken(req.user, req.auth_origin);
const decoded = jwt.decode(accessToken);

return res
.cookie('token', accessToken, { httpOnly: true })
.status(200)
.json({
user: req.user,
expiresAt: decoded.exp * 1000, // Convert JWT exp (seconds) to milliseconds
});
};

// Update user's managedProjects
Expand Down
173 changes: 154 additions & 19 deletions backend/middleware/auth.middleware.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,158 @@
const jwt = require('jsonwebtoken');
const { CONFIG_AUTH } = require('../config');

function verifyToken(req, res, next) {
// Allow users to set token
// eslint-disable-next-line dot-notation
let token = req.headers['x-access-token'] || req.headers['authorization'];
if (token.startsWith('Bearer ')) {
// Remove Bearer from string
token = token.slice(7, token.length);
const { RefreshToken, User } = require('../models');
const crypto = require('crypto');
const AuthUtils = require('../../shared/authorizationUtils');

const SECRET = CONFIG_AUTH.TOKEN_SECRET;

// Utility functions

function generateAccessToken(user, auth_origin) {
return jwt.sign(
{
id: user._id,
email: user.email,
role: user.accessLevel,
accessLevel: user.accessLevel,
auth_origin: auth_origin,
},
SECRET,
{ expiresIn: CONFIG_AUTH.ACCESS_TOKEN_EXPIRATION },
);
}

function generateRefreshToken() {
return crypto.randomBytes(32).toString('hex');
}

function hashToken(token) {
return crypto.createHash('sha256').update(token).digest('hex');
}

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();
}
if (!token) {
return res.sendStatus(403);

// 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'];

if (!accessToken) {
return res.status(401).json({ error: 'Access token required' });
}

if (accessToken.startsWith('Bearer ')) {
accessToken = accessToken.slice(7, accessToken.length);
}

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

next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}

if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'Invalid token' });
}

return res.status(401).json({ error: 'Authentication failed' });
}
}

// shorthand for authenticateAccessToken
const authUser = authenticateAccessToken;

async function authenticateRefreshToken(req, res, next) {
try {
const decoded = jwt.verify(token, CONFIG_AUTH.SECRET);
res.cookie('token', token, { httpOnly: true });
req.userId = decoded.id;
return next();
} catch (err) {
return res.sendStatus(401);
const refreshToken = req.cookies?.refresh_token;

if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}

const tokenHash = hashToken(refreshToken);

const tokenDoc = await RefreshToken.findOne({
hash: tokenHash,
expiresAt: { $gt: new Date() },
});

if (!tokenDoc) {
return res.status(401).json({ error: 'Invalid or expired refresh token' });
}

const user = await User.findById(tokenDoc.userId);
if (!user) {
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;

next();
} catch (error) {
console.error('Refresh token validation error:', error);
return res.status(401).json({ error: 'Authentication failed' });
}
}

function requireRole(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}

if (!AuthUtils.hasAnyRole(req.user, roles)) {
return res.status(403).json({
error: 'Insufficient permissions',
required_role: roles,
your_role: req.user.accessLevel,
});
}

next();
};
}

function requireMinimumRole(role) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}

const user = req.user;
if (!AuthUtils.hasMinimumRole(user, role)) {
return res.status(403).json({
error: 'Insufficient permissions',
required_minimum_role: role,
your_role: req.user.accessLevel,
});
}
next();
};
}

function verifyCookie(req, res, next) {
jwt.verify(req.cookies.token, CONFIG_AUTH.SECRET, (err, decoded) => {
jwt.verify(req.cookies.token, SECRET, (err, decoded) => {
if (err) {
return res.sendStatus(401);
}
Expand All @@ -35,8 +163,15 @@ function verifyCookie(req, res, next) {
});
}

const AuthUtil = {
verifyToken,
module.exports = {
authenticateAccessToken,
authUser,
authenticateRefreshToken,
requireRole,
requireMinimumRole,
generateAccessToken,
generateRefreshToken,
getClientIp,
hashToken,
verifyCookie,
};
module.exports = AuthUtil;
6 changes: 2 additions & 4 deletions backend/middleware/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
const AuthUtil = require('./auth.middleware');
const Auth = require('./auth.middleware');
const verifyUser = require('./user.middleware');
const verifyToken = require('./token.middleware');

module.exports = {
AuthUtil,
Auth,
verifyUser,
verifyToken,
};
27 changes: 0 additions & 27 deletions backend/middleware/token.middleware.js

This file was deleted.

4 changes: 3 additions & 1 deletion backend/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ const { Question } = require('./question.model');
const { RecurringEvent } = require('./recurringEvent.model');
const { Role } = require('./role.model');
const { User } = require('./user.model');
const { RefreshToken } = require('./refreshToken.model');

const mongoose = require("mongoose");
const mongoose = require('mongoose');
mongoose.Promise = global.Promise;

module.exports = {
Expand All @@ -19,4 +20,5 @@ module.exports = {
RecurringEvent,
Role,
User,
RefreshToken,
};
Loading