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
39 changes: 18 additions & 21 deletions backend/app.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@

// app.js - Entry point for our application

import { createRequire } from 'module';
import cookieParser from 'cookie-parser';
// Load in all of our node modules. Their uses are explained below as they are called.
import express from 'express';
import morgan from 'morgan';
import cron from 'node-cron';
import fetch from 'node-fetch';
import cookieParser from 'cookie-parser';
import swaggerUi from 'swagger-ui-express';
import { createRequire } from 'module';

const require = createRequire(import.meta.url);
let swaggerDocument;
Expand All @@ -20,14 +19,12 @@ try {
}

const customRequestHeaderName = 'x-customrequired-header';
const dontCheckCustomRequestHeaderApis = ['GET::/api/recurringevents', 'GET::/api/healthcheck', 'GET::/api-docs', 'GET::/api-docs/'];

// Import environment variables
import dotenv from 'dotenv';
import dotenvExpand from 'dotenv-expand';

const myEnv = dotenv.config();
dotenvExpand(myEnv);
const dontCheckCustomRequestHeaderApis = [
'GET::/api/recurringevents',
'GET::/api/healthcheck',
'GET::/api-docs',
'GET::/api-docs/',
];

// Verify environment variables
import assertEnv from 'assert-env';
Expand Down Expand Up @@ -69,10 +66,10 @@ if (swaggerDocument) {
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
}

// WORKERS
import openCheckins from './workers/openCheckins.js';
import closeCheckins from './workers/closeCheckins.js';
import createRecurringEvents from './workers/createRecurringEvents.js';
// WORKERS
import openCheckins from './workers/openCheckins.js';
const runOpenCheckinWorker = openCheckins(cron, fetch);
const runCloseCheckinWorker = closeCheckins(cron, fetch);
const runCreateRecurringEventsWorker = createRecurringEvents(cron, fetch);
Expand All @@ -86,19 +83,19 @@ cleanupExpiredTokens();
// MIDDLEWARE
import errorhandler from './middleware/errorhandler.middleware.js';

// ROUTES
import eventsRouter from './routers/events.router.js';
//import slackRouter from './routers/slack.router.js';
import authRouter from './routers/auth.router.js';
import checkInsRouter from './routers/checkIns.router.js';
import usersRouter from './routers/users.router.js';
import questionsRouter from './routers/questions.router.js';
import checkUserRouter from './routers/checkUser.router.js';
// ROUTES
import eventsRouter from './routers/events.router.js';
import grantPermissionRouter from './routers/grantpermission.router.js';
import healthCheckRouter from './routers/healthCheck.router.js';
import projectTeamMembersRouter from './routers/projectTeamMembers.router.js';
import projectsRouter from './routers/projects.router.js';
import questionsRouter from './routers/questions.router.js';
import recurringEventsRouter from './routers/recurringEvents.router.js';
import projectTeamMembersRouter from './routers/projectTeamMembers.router.js';
//import slackRouter from './routers/slack.router.js';
import authRouter from './routers/auth.router.js';
import healthCheckRouter from './routers/healthCheck.router.js';
import usersRouter from './routers/users.router.js';

// 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
14 changes: 14 additions & 0 deletions backend/env.bootstrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
import dotenvExpand from 'dotenv-expand';

const envFilePath = fileURLToPath(new URL('./.env', import.meta.url));
const envResult = dotenv.config({ path: envFilePath });

if (envResult.error && envResult.error.code !== 'ENOENT') {
throw envResult.error;
}

if (!envResult.error || envResult.parsed) {
dotenvExpand(envResult);
}
8 changes: 4 additions & 4 deletions backend/middleware/auth.middleware.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import { hasAnyRole, hasMinimumRole } from '../../shared/authorizationUtils.js';
import { CONFIG_AUTH } from '../config/index.js';
import { RefreshToken, User } from '../models/index.js';
import AuthUtils from '../../shared/authorizationUtils.js';

const SECRET = CONFIG_AUTH.JWT_SECRET;

Expand Down Expand Up @@ -113,7 +113,7 @@ function requireRole(...roles) {
return res.status(401).json({ error: 'Authentication required' });
}

if (!AuthUtils.hasAnyRole(req.user, roles)) {
if (!hasAnyRole(req.user, ...roles)) {
return res.status(403).json({
error: 'Insufficient permissions',
required_role: roles,
Expand All @@ -132,7 +132,7 @@ function requireMinimumRole(role) {
}

const user = req.user;
if (!AuthUtils.hasMinimumRole(user, role)) {
if (!hasMinimumRole(user, role)) {
return res.status(403).json({
error: 'Insufficient permissions',
required_minimum_role: role,
Expand Down
13 changes: 9 additions & 4 deletions backend/models/refreshToken.model.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const mongoose = require('mongoose');
const { CONFIG_AUTH } = require('../config');
import mongoose from 'mongoose';
import { CONFIG_AUTH } from '../config/index.js';

mongoose.Promise = global.Promise;

Expand All @@ -11,7 +11,12 @@ const refreshTokenSchema = mongoose.Schema({
index: true,
},
hash: { type: String, required: true, unique: true, immutable: true },
createdAt: { type: Date, required: true, default: () => Date.now(), immutable: true },
createdAt: {
type: Date,
required: true,
default: () => Date.now(),
immutable: true,
},
expiresAt: {
type: Date,
required: true,
Expand Down Expand Up @@ -39,4 +44,4 @@ refreshTokenSchema.index({ expires_at: 1 }, { expiresAfterSeconds: 0 });

const RefreshToken = mongoose.model('RefreshToken', refreshTokenSchema);

module.exports = { RefreshToken };
export { RefreshToken };
45 changes: 10 additions & 35 deletions backend/models/user.model.test.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
// import necessary modules
const mongoose = require('mongoose');
const { User } = require('./user.model');
import { describe, it, expect, vi, afterEach } from 'vitest';
import mongoose from 'mongoose';
import { User } from './user.model.js';

describe('Unit tests for User Model', () => {
// Clears all mocks after each test
afterEach(() => {
jest.restoreAllMocks();
vi.restoreAllMocks();
});

describe('Serialization test', () => {
it('should return the correct serialized user object', async () => {
// Create mock user data
const userObj = {
_id: new mongoose.Types.ObjectId(),
name: {
firstName: 'mock',
lastName: 'user',
},
name: { firstName: 'mock', lastName: 'user' },
email: 'mock.user@example.com',
accessLevel: 'user',
createdDate: new Date(),
Expand All @@ -39,17 +34,12 @@ describe('Unit tests for User Model', () => {
isActive: true,
};

// Create a mock user instance
const mockUser = new User(userObj);
const serializedUser = mockUser.serialize();

// Test
expect(serializedUser).toEqual({
id: mockUser._id,
name: {
firstName: mockUser.name.firstName,
lastName: mockUser.name.lastName,
},
name: { firstName: mockUser.name.firstName, lastName: mockUser.name.lastName },
email: mockUser.email,
accessLevel: mockUser.accessLevel,
createdDate: mockUser.createdDate,
Expand All @@ -76,60 +66,45 @@ describe('Unit tests for User Model', () => {

describe('Validation test', () => {
it('should fail validation check if accessLevel is invalid', async () => {
// Create a mock user with an invalid accessLevel
const mockuser = new User({
accessLevel: 'projectleader', // not 'user', 'admin', or 'superadmin'
});
const mockuser = new User({ accessLevel: 'projectleader' });

// Attempt to validate the mock user by checking for valid accessLevel
let error;
try {
await mockuser.validate();
} catch (err) {
error = err;
}

// Tests
expect(error).toBeDefined();
expect(error.errors.accessLevel).toBeDefined();
});

it('should enforce that emails are stored in lowercase', async () => {
// Create a mock user with an uppercase email
const uppercaseEmail = 'TEST@test.com';
const mockUser = new User({
email: uppercaseEmail,
});
const mockUser = new User({ email: uppercaseEmail });

mockUser.validate();
// Tests
expect(mockUser.email).toBe(uppercaseEmail.toLowerCase());
});

it('should pass validation with valid user data', async () => {
// Create a mock user with valid data
const mockUser = new User({
name: {
firstName: 'Valid',
lastName: 'User',
},
name: { firstName: 'Valid', lastName: 'User' },
email: 'mockuser@gmail.com',
accessLevel: 'user',
});

// Attempt to save the mock user
let error;
try {
await mockUser.validate();
} catch (err) {
error = err;
}

// Tests
expect(error).toBeUndefined();
expect(mockUser.email).toBe('mockuser@gmail.com');
expect(mockUser.accessLevel).toBe('user');
await expect(mockUser.validate()).resolves.toBeUndefined(); // async validation check
await expect(mockUser.validate()).resolves.toBeUndefined();
});
});
});
21 changes: 13 additions & 8 deletions backend/routers/auth.router.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ vi.mock('../controllers/user.controller');
vi.mock('../controllers/email.controller');
vi.mock('../models/user.model');
// Set up mocks for middleware
vi.mock('../middleware', () => ({
AuthUtil: {
verifyCookie: vi.fn((req, res, next) => next()),
},
vi.mock('../middleware/index.js', () => ({
Auth: {
authUser: vi.fn((req, res, next) => next()),
},
AuthUtil: {
verifyCookie: vi.fn((req, res, next) => next()),
},
verifyUser: {
checkDuplicateEmail: vi.fn((req, res, next) => next()),
isAdminByEmail: vi.fn((req, res, next) => next()),
Expand All @@ -20,10 +20,16 @@ vi.mock('../middleware', () => ({
isTokenValid: vi.fn((req, res, next) => next()),
},
}));
vi.mock('../middleware/auth.middleware.js', () => ({
default: {},
authenticateRefreshToken: vi.fn((req, res, next) => next()),
}));
// Set up mocks for authApiValidator
vi.mock('../validators/user.api.validator', () => ({
validateCreateUserAPICall: vi.fn((req, res, next) => next()),
validateSigninUserAPICall: vi.fn((req, res, next) => next()),
vi.mock('../validators/index.js', () => ({
authApiValidator: {
validateCreateUserAPICall: vi.fn((req, res, next) => next()),
validateSigninUserAPICall: vi.fn((req, res, next) => next()),
},
}));

// Import User model and controller
Expand Down Expand Up @@ -115,7 +121,6 @@ describe('Unit tests for auth router', () => {
});

expect(authApiValidator.validateSigninUserAPICall).toHaveBeenCalled();
expect(verifyUser.isAdminByEmail).toHaveBeenCalled();
expect(UserController.signin).toHaveBeenCalled();
expect(EmailController.sendLoginLink).toHaveBeenCalledWith(
email,
Expand Down
Loading
Loading