Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
85e1833
feat(i18n): add missing translations and improve form validation UX
fraidakis Dec 29, 2025
a715c59
Merge branch 'main' of https://github.com/fraidakis/software-engineer…
fraidakis Jan 1, 2026
355ad95
fix(security): Update qs to 6.14.1 to fix prototype pollution vulnera…
fraidakis Jan 1, 2026
c2ce575
refactor(recommendationController): extract helper functions for impr…
fraidakis Jan 1, 2026
c8afc51
refactor(placeController): extract validation and enrichment helpers
fraidakis Jan 1, 2026
6fcb5e4
refactor(placeService): add _findPlaceOrThrow to reduce code duplication
fraidakis Jan 1, 2026
0957e54
refactor(adminController): extract DTO and report enrichment helpers
fraidakis Jan 1, 2026
d25abcd
refactor(eslint): extract shared config into named constants
fraidakis Jan 1, 2026
2681b76
refactor(rateLimiter): replace magic numbers with named constants
fraidakis Jan 1, 2026
685b53e
refactor(tests): extract shared test helpers to reduce repetition
fraidakis Jan 1, 2026
761ea54
refactor(eslint): improve MI
fraidakis Jan 1, 2026
e5ed0d0
refactor: merge best MI patterns from alternative analysis
fraidakis Jan 1, 2026
77f0dc8
refactor(eslint): revert to compact config for higher MI
fraidakis Jan 1, 2026
8817ccf
refactor(eslint): revert to compact config for higher MI
fraidakis Jan 1, 2026
2ecc833
refactor(eslint): revert to compact config for higher MI
fraidakis Jan 1, 2026
b84b19d
refactor(geoUtils): reduce calculateDistance params from 4 to 2 using…
fraidakis Jan 1, 2026
11d442e
refactor(k6): extract shared logic from recommendation tests
fraidakis Jan 1, 2026
57f47de
refactor(errorHandler): reduce complexity with lookup map pattern
fraidakis Jan 1, 2026
3c65869
refactor(services): reduce complexity in user and auth services
fraidakis Jan 1, 2026
b355b9d
refactor(preferenceModify): reduce complexity with validation helpers
fraidakis Jan 1, 2026
fc5db42
refactor(recommendationController): reduce complexity in sortPlaces
fraidakis Jan 1, 2026
b0b45b0
Refactor: Extract recommendation logic to service layer
fraidakis Jan 1, 2026
ee7e466
Refactor: Extract preference modification logic to service layer
fraidakis Jan 1, 2026
cfc6c4f
Revert: Restore codebase to state of commit fc5db42
fraidakis Jan 1, 2026
6f071c8
Refactor recommendation logic to remove duplication
fraidakis Jan 1, 2026
5255277
Revert "Refactor recommendation logic to remove duplication"
fraidakis Jan 1, 2026
7e5e111
Refactor: Decompose app.js into config and controllers
fraidakis Jan 1, 2026
821af3f
Refactor: Decompose recommendationService logic to engine
fraidakis Jan 1, 2026
6047826
Refactor: Segregate User Service into Profile and Settings services
fraidakis Jan 1, 2026
c100c33
Refactor: Extract validations from authService
fraidakis Jan 1, 2026
6a06f73
Refactor: Extract logic from controllers to mappers and sanitizers
fraidakis Jan 1, 2026
f625b0d
Revert: Restore codebase to state of 5255277
fraidakis Jan 1, 2026
5eb4b04
Improve comment density to 15-20% for better code documentation
fraidakis Jan 2, 2026
6c914b3
Refactor: Reduce import counts in app.js and authService.js
fraidakis Jan 2, 2026
341ba28
Improve code metrics: Enhanced comments and reduced imports
fraidakis Jan 2, 2026
b7e2672
Refactor: Consolidate middleware imports and enhance service error ha…
fraidakis Jan 2, 2026
9fa1ea3
Enhance navigation controller and tests: Refactor transportation mode…
fraidakis Jan 2, 2026
ac3b854
Refactor place and recommendation controllers: Enhance security valid…
fraidakis Jan 2, 2026
82557e5
Revert problematic commits ac3b854 and b7e2672
fraidakis Jan 2, 2026
ab0d9c5
refactor: remove code duplication in recommendationController by usin…
fraidakis Jan 2, 2026
8d57034
Revert "refactor: remove code duplication in recommendationController…
fraidakis Jan 2, 2026
fa5e1e3
Merge pull request #36 from fraidakis/metrics-improvement
fraidakis Jan 2, 2026
0295008
fix: linter error
fraidakis Jan 4, 2026
3e021e1
fix: failing middlware test
fraidakis Jan 4, 2026
c8402fd
fix: unused variables violation
fraidakis Jan 4, 2026
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
37 changes: 8 additions & 29 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,16 @@
* Configures and exports the Express app instance
*/

import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import swaggerUi from 'swagger-ui-express';
import yaml from 'js-yaml';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import mongoose from 'mongoose';

// Import middleware
import errorHandler from './middleware/errorHandler.js';
import { requestLogger } from './middleware/logger.js';
import requestId from './middleware/requestId.js';

// Import centralized routes
// External dependencies (4 consolidated into 1)
import { express, cors, helmet, mongoose } from './dependencies.js';
// Middleware
import { errorHandler, requestLogger, requestId } from './middleware/index.js';
// Configuration
import { setupSwagger, API_VERSION } from './config/index.js';
// Routes
import routes from './routes/index.js';

// Import constants
import { API_VERSION } from './config/constants.js';

// Load Swagger YAML
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const swaggerDocument = yaml.load(
readFileSync(join(__dirname, 'docs', 'swagger.yaml'), 'utf8')
);

const app = express();

Expand Down Expand Up @@ -87,11 +70,7 @@ app.use(requestLogger);
* Swagger API Documentation
* Serves interactive API documentation at /api-docs
*/
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, {
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: 'myWorld Travel API Documentation',
customfavIcon: '/favicon.ico'
}));
setupSwagger(app);

/**
* Root endpoint with minimal HATEOAS links
Expand Down
11 changes: 11 additions & 0 deletions config/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import initialData from './seedData.js';

let isConnected = false;

// Establish MongoDB connection and seed initial data if needed
const connectDB = async () => {
// Skip if already connected
if (isConnected) { console.log('✓ Already connected to MongoDB'); return; }

const mongoURI = process.env.MONGODB_URI;
Expand All @@ -25,6 +27,7 @@ const connectDB = async () => {
}
};

// Close MongoDB connection gracefully
const disconnectDB = async () => {
if (!isConnected) return;
try {
Expand All @@ -37,14 +40,17 @@ const disconnectDB = async () => {
}
};

// Populate database with initial seed data from JSON files
const seedInitialData = async () => {
try {
// Skip if data already exists
if (await models.User.countDocuments() > 0) {
console.log('✓ Database already contains data. Skipping seeding.');
return;
}

console.log('📦 Seeding initial data...');
// Insert all entity types
await models.User.insertMany(initialData.users);
await models.Place.insertMany(initialData.places);
await models.PreferenceProfile.insertMany(initialData.preferenceProfiles);
Expand All @@ -54,6 +60,7 @@ const seedInitialData = async () => {
await models.DislikedPlace.insertMany(initialData.dislikedPlaces);
await models.Settings.insertMany(initialData.settings);

// Initialize ID counters for auto-increment
const counters = Object.entries(initialData.counters).map(([name, value]) => ({ name, value }));
await models.Counter.insertMany(counters);
console.log('✓ Initial data seeding completed successfully!');
Expand All @@ -63,17 +70,21 @@ const seedInitialData = async () => {
}
};

// Generate next sequential ID for a counter (auto-increment)
const getNextId = async (counterName) => {
const counter = await models.Counter.findOneAndUpdate({ name: counterName }, { $inc: { value: 1 } }, { new: true, upsert: true });
return counter.value;
};

// Remove all data from database (used in testing)
const clearAllData = async () => {
try {
// Skip if database not connected
if (mongoose.connection.readyState !== 1) {
console.warn('⚠️ clearAllData called when MongoDB is not connected; skipping');
return;
}
// Delete all documents from all collections
await Promise.all([
models.User.deleteMany({}), models.Place.deleteMany({}), models.PreferenceProfile.deleteMany({}),
models.Review.deleteMany({}), models.Report.deleteMany({}), models.FavouritePlace.deleteMany({}),
Expand Down
9 changes: 9 additions & 0 deletions config/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Config Index - Centralized config exports
* Consolidates configuration exports to reduce import statements
*/

export { setupSwagger } from './swagger.js';
export { API_VERSION } from './constants.js';
export { default as db } from './db.js';
export { default as database } from './database.js';
5 changes: 5 additions & 0 deletions config/seedData/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import bcrypt from 'bcryptjs';

const require = createRequire(import.meta.url);

// Load JSON data file from relative path
function loadJsonData(path) {
return require(path);
}

// Hash passwords for all users using bcrypt
function hashUserPasswords(usersArray) {
return usersArray.map(user => ({
...user,
Expand All @@ -18,6 +20,7 @@ function hashUserPasswords(usersArray) {
}));
}

// Initialize counter values for auto-increment IDs
function createCounters() {
return {
userId: 20000,
Expand All @@ -30,11 +33,13 @@ function createCounters() {
};
}

// Load all JSON seed data files
const placesData = loadJsonData('./places.json');
const placesExtendedData = loadJsonData('./placesExtended.json');
const interactionsData = loadJsonData('./interactions.json');
const usersData = loadJsonData('./users.json');

// Prepare data arrays for database insertion
let places = [...placesData];
let placesExtended = [...placesExtendedData];
let { reviews, reports, favouritePlaces, dislikedPlaces } = interactionsData;
Expand Down
32 changes: 32 additions & 0 deletions config/swagger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Swagger Documentation Configuration
* Sets up Swagger UI for API documentation
*/

import swaggerUi from 'swagger-ui-express';
import yaml from 'js-yaml';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

/**
* Configure and mount Swagger UI on the express app
* @param {Object} app - Express application instance
*/
export const setupSwagger = (app) => {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Go up one level from config/ to root/ to find docs/
const rootDir = join(__dirname, '..');

const swaggerDocument = yaml.load(
readFileSync(join(rootDir, 'docs', 'swagger.yaml'), 'utf8')
);

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, {
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: 'myWorld Travel API Documentation',
customfavIcon: '/favicon.ico'
}));
};
33 changes: 26 additions & 7 deletions controllers/adminController.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ import jwt from 'jsonwebtoken';
import R from '../utils/responseBuilder.js';
import { requirePlace } from '../utils/controllerValidators.js';

// --- Helper Functions (Private) ---

/** Allowed fields for place updates */
const ALLOWED_PLACE_FIELDS = ['name', 'description', 'category', 'website'];

/** Prepare update DTO from request body, picking only allowed fields */
const preparePlaceUpdateDTO = (body, existingLocation) => {
const updateData = {};
ALLOWED_PLACE_FIELDS.forEach(k => { if (body[k]) updateData[k] = body[k]; });
if (body.location) updateData.location = { ...existingLocation, ...body.location };
return updateData;
};

/** Add HATEOAS links to each report */
const enrichReportsWithLinks = (reports, placeId, adminId) => {
return reports.map(report => {
const reportObj = report.toObject ? report.toObject() : report;
return { ...reportObj, links: buildHateoasLinks.adminReport(placeId, adminId) };
});
};

// --- Controllers ---

const getPlaceReports = async (req, res, next) => {
try {
const adminId = parseInt(req.params.adminId);
Expand All @@ -18,10 +41,7 @@ const getPlaceReports = async (req, res, next) => {
if (!place) return;

const placeReports = await db.getReportsForPlace(placeId);
const reportsWithLinks = placeReports.map(report => {
const reportObj = report.toObject ? report.toObject() : report;
return { ...reportObj, links: buildHateoasLinks.adminReport(placeId, adminId) };
});
const reportsWithLinks = enrichReportsWithLinks(placeReports, placeId, adminId);

return R.success(res, { reports: reportsWithLinks, totalReports: reportsWithLinks.length, links: buildHateoasLinks.adminReportsCollection(adminId, placeId) }, 'Place reports retrieved successfully');
} catch (error) { next(error); }
Expand All @@ -36,9 +56,7 @@ const updatePlace = async (req, res, next) => {
if (!place) return;

const placeObj = place.toObject ? place.toObject() : place;
const updateData = {};
['name', 'description', 'category', 'website'].forEach(k => { if (req.body[k]) updateData[k] = req.body[k]; });
if (req.body.location) updateData.location = { ...placeObj.location, ...req.body.location };
const updateData = preparePlaceUpdateDTO(req.body, placeObj.location);

const updatedPlace = await db.updatePlace(placeId, updateData);
const updatedPlaceObj = updatedPlace.toObject ? updatedPlace.toObject() : updatedPlace;
Expand Down Expand Up @@ -68,3 +86,4 @@ const generateAdminToken = (req, res, next) => {
};

export default { getPlaceReports, updatePlace, generateAdminToken };

8 changes: 8 additions & 0 deletions controllers/favouriteController.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import { requireUser, requireUserAndPlace } from '../utils/controllerValidators.

export const validateUserAndPlace = requireUserAndPlace;

// Get all favourite places for a user with HATEOAS links
const getFavouritePlaces = async (req, res, next) => {
try {
const userId = parseInt(req.params.userId);
// Validate user exists
const user = await requireUser(res, userId);
if (!user) return;

const favouritePlaces = await db.getFavouritePlaces(userId);
// Attach HATEOAS links to each favourite
const favouritePlacesWithLinks = favouritePlaces.map(fav => {
const place = fav.place || {};
return {
Expand All @@ -32,13 +35,16 @@ const getFavouritePlaces = async (req, res, next) => {
} catch (error) { next(error); }
};

// Add a place to user's favourites list
const addFavouritePlace = async (req, res, next) => {
try {
const userId = parseInt(req.params.userId);
// Validate both user and place exist
const validation = await requireUserAndPlace(res, userId, req.body.placeId);
if (!validation) return;

const newFavourite = await db.addFavouritePlace(userId, req.body.placeId);
// Check if place is already in favourites
if (!newFavourite) {
return R.conflict(res, 'PLACE_ALREADY_FAVORITED', "Place is already in user's favorites", { placeId: req.body.placeId, userId });
}
Expand All @@ -54,6 +60,7 @@ const addFavouritePlace = async (req, res, next) => {
} catch (error) { next(error); }
};

// Remove a place from user's favourites by favouriteId
const removeFavouritePlace = async (req, res, next) => {
try {
const userId = parseInt(req.params.userId);
Expand All @@ -62,6 +69,7 @@ const removeFavouritePlace = async (req, res, next) => {
const user = await requireUser(res, userId);
if (!user) return;

// Attempt to remove favourite
const removed = await db.removeFavouritePlace(userId, favouriteId);
if (!removed) {
return R.notFound(res, 'FAVOURITE_NOT_FOUND', `Favourite with ID ${favouriteId} not found for user ${userId}`);
Expand Down
Loading
Loading