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
17 changes: 17 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Server
PORT=5000
NODE_ENV=development

# Database
DB_HOST=localhost
DB_PORT=3306
DB_NAME=smartqueue
DB_USER=root
DB_PASSWORD=

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a placeholder password to avoid encouraging empty passwords.

The empty DB_PASSWORD value, even in an example file, may encourage developers to run local environments without passwords, creating a security gap.

🔒 Proposed fix
-DB_PASSWORD=
+DB_PASSWORD=your-secure-password-here
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DB_PASSWORD=
DB_PASSWORD=your-secure-password-here
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 10-10: [UnorderedKey] The DB_PASSWORD key should go before the DB_PORT key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/.env.example` at line 10, Replace the empty DB_PASSWORD value in the
example env with a non-empty placeholder (e.g., a descriptive token like
"CHANGE_ME_DB_PASSWORD" or "YOUR_DB_PASSWORD") so the example discourages using
empty/blank database passwords; update the DB_PASSWORD entry in
backend/.env.example accordingly.


# Auth
JWT_SECRET=change-me-to-a-strong-random-value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Strengthen the JWT_SECRET placeholder to convey minimum security requirements.

The generic placeholder doesn't communicate that JWT HS256 secrets should be at least 256 bits (32+ random bytes). A weak secret undermines the entire authentication system.

🔐 Proposed fix
-JWT_SECRET=change-me-to-a-strong-random-value
+JWT_SECRET=use-at-least-32-random-bytes-base64-encoded-minimum-256-bits

Or add a comment above:

+# JWT_SECRET must be at least 256 bits (32 bytes). Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
 JWT_SECRET=change-me-to-a-strong-random-value
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
JWT_SECRET=change-me-to-a-strong-random-value
JWT_SECRET=use-at-least-32-random-bytes-base64-encoded-minimum-256-bits
Suggested change
JWT_SECRET=change-me-to-a-strong-random-value
# JWT_SECRET must be at least 256 bits (32 bytes). Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
JWT_SECRET=change-me-to-a-strong-random-value
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/.env.example` at line 13, Update the JWT_SECRET placeholder in
.env.example to clearly require a strong HS256 secret: replace the generic
"change-me" value with a descriptive placeholder and add a comment above
JWT_SECRET explaining it must be at least 256 bits (≥32 random bytes) or a
base64-encoded 32+ byte value, generated from a CSPRNG; reference the JWT_SECRET
variable so reviewers can find and verify the change.

JWT_EXPIRES_IN=24h

# CORS — comma-separated allowed origins
CORS_ORIGIN=http://localhost:5173
41 changes: 41 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"express-rate-limit": "^8.5.2",
"helmet": "^8.2.0",
"jsonwebtoken": "^9.0.3",
"morgan": "^1.11.0",
"mysql2": "^3.22.4",
Expand Down
18 changes: 15 additions & 3 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
// 1. Load the environment variables FIRST
require('dotenv').config();
console.log("DEBUG -> JWT_SECRET VALUE IS:", process.env.JWT_SECRET);
// 2. NOW load your app, database, and models

// 2. Validate required secrets before booting
if (!process.env.JWT_SECRET) {
console.error('FATAL: JWT_SECRET environment variable is not set. Aborting.');
process.exit(1);
}

// 3. NOW load your app, database, and models
const app = require('./src/app');
const sequelize = require('./src/config/database');
require('./src/models');

const PORT = process.env.PORT || 5000;
const isDev = process.env.NODE_ENV !== 'production';

async function startServer() {
try {
await sequelize.authenticate();
console.log('Database connected successfully');

await sequelize.sync({ alter: true });
// Only auto-alter schema in development; use migrations in production
if (isDev) {
await sequelize.sync({ alter: true });
} else {
await sequelize.sync();
}
console.log('Models synchronized');

app.listen(PORT, () => {
Expand Down
37 changes: 32 additions & 5 deletions backend/src/app.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const app = express();

const authRoutes = require('./routes/auth.routes');

// Global Middleware
app.use(cors());
app.use(express.json());
app.use(morgan('dev'));
// Security headers
app.use(helmet());

// CORS — restrict to known frontend origin
const allowedOrigins = process.env.CORS_ORIGIN
? process.env.CORS_ORIGIN.split(',')
: [];

app.use(
cors({
origin(origin, callback) {
// Allow server-to-server requests (no origin) and allowed origins
if (!origin || allowedOrigins.includes(origin)) {
return callback(null, true);
}
return callback(new Error('Not allowed by CORS'));
},
credentials: true,
})
);

// Body parser with size limit to prevent large-payload DoS
app.use(express.json({ limit: '10kb' }));

// Request logging — verbose in dev, minimal in production
if (process.env.NODE_ENV === 'production') {
app.use(morgan('combined'));
} else {
app.use(morgan('dev'));
}

// API Routes
app.use('/api/auth', authRoutes); // Handles both /api/auth/register and /api/auth/login
app.use('/api/auth', authRoutes);

// Health Check
app.get('/api/health', (req, res) => {
Expand Down
27 changes: 27 additions & 0 deletions backend/src/middleware/auth.middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const jwt = require('jsonwebtoken');

const authenticate = (req, res, next) => {
const header = req.headers.authorization;

if (!header || !header.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: 'Authentication required.',
});
}

const token = header.split(' ')[1];

try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
Comment on lines +15 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

jsonwebtoken 9.x jwt.verify algorithms option default behavior

💡 Result:

In jsonwebtoken version 9.x, the jwt.verify function does not accept unsigned tokens ("alg": "none") by default [1]. If you need to verify an unsigned token, you must explicitly include 'none' in the algorithms option [1]. When the algorithms option is not specified, jwt.verify defaults to a list of allowed algorithms based on the type of key provided [2][3]: - Secret (HMAC): ['HS256', 'HS384', 'HS512'] [2][3] - RSA: ['RS256', 'RS384', 'RS512'] [2][3] - EC (Elliptic Curve): ['ES256', 'ES384', 'ES512'] [2][3] - Default (if key type cannot be inferred): ['RS256', 'RS384', 'RS512'] [2][3] To verify an unsigned token, you must pass { algorithms: ['none'] } in the options argument [1]. It is strongly recommended to always explicitly specify the expected algorithms to prevent security vulnerabilities, such as algorithm confusion attacks [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the auth controller and the middleware file mentioned in the review
rg -n "jwt\\.sign\\(" backend/src --hidden || true
rg -n "JWT_SECRET|jwt\\.verify\\(" backend/src/middleware/auth.middleware.js backend/src || true

# Print the relevant sections with line numbers (keep output small)
echo "---- auth.controller.js jwt.sign context ----"
for f in $(fd -t f "auth.controller.js" backend/src -a 2>/dev/null || true); do
  echo "FILE: $f"
  sed -n '1,220p' "$f" | nl -ba | sed -n '1,220p'
done

echo "---- auth.middleware.js jwt.verify context ----"
if [ -f "backend/src/middleware/auth.middleware.js" ]; then
  sed -n '1,120p' backend/src/middleware/auth.middleware.js | nl -ba | sed -n '1,120p'
fi

Repository: 0xpritam/SmartQueue

Length of output: 605


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- backend/src/controllers/auth.controller.js around jwt.sign ----"
sed -n '70,130p' backend/src/controllers/auth.controller.js | cat -n

echo "---- backend/src/middleware/auth.middleware.js around jwt.verify ----"
sed -n '1,80p' backend/src/middleware/auth.middleware.js | cat -n

Repository: 0xpritam/SmartQueue

Length of output: 2772


Pin the expected algorithm in jwt.verify.

auth.controller.js signs with a symmetric secret but does not specify an algorithm; jsonwebtoken will default to HS256 for signing, while jwt.verify currently accepts multiple HS* algorithms. Restricting verification to HS256 prevents accepting tokens signed with unexpected algorithms.

🔒 Proposed change
-    const decoded = jwt.verify(token, process.env.JWT_SECRET);
+    const decoded = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/middleware/auth.middleware.js` around lines 15 - 16, The
jwt.verify call in the auth middleware should be restricted to the expected
algorithm to avoid accepting other HS* variants: update the jwt.verify(token,
process.env.JWT_SECRET) invocation in auth.middleware.js to pass verification
options specifying algorithms: ['HS256'] (matching the signing in
auth.controller.js) so only HS256-signed tokens are accepted; keep the rest of
the try/catch flow unchanged.

req.user = decoded;
return next();
} catch (err) {
return res.status(401).json({
success: false,
message: 'Invalid or expired token.',
});
}
};

module.exports = { authenticate };
4 changes: 4 additions & 0 deletions backend/src/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ module.exports = (sequelize) => {
notEmpty: {
msg: 'Name is required',
},
len: {
args: [1, 100],
msg: 'Name must be between 1 and 100 characters',
},
},
},

Expand Down
20 changes: 14 additions & 6 deletions backend/src/routes/auth.routes.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
const express = require('express');
const rateLimit = require('express-rate-limit');
const router = express.Router();

// 1. Add 'login' to the destructured import
const { register, login } = require('../controllers/auth.controller');

// POST /api/auth/register
router.post('/register', register);
// Rate-limit auth endpoints to mitigate brute-force attacks
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 20, // 20 attempts per window per IP
standardHeaders: true,
legacyHeaders: false,
message: {
success: false,
message: 'Too many requests, please try again later.',
},
});
Comment on lines +8 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

express-rate-limit v8 max option deprecated limit

💡 Result:

In express-rate-limit, the max option is not deprecated in the sense of being removed; rather, it has been superseded by the limit option [1][2]. The option was renamed from max to limit starting in version 7.x to better align with IETF standard draft terminology [2][3]. The package continues to support max for backwards compatibility, meaning existing code using max will still function as expected [1][2]. However, the limit option is the current, preferred configuration property [4][5]. It is recommended to update your configuration to use limit to ensure future compatibility and avoid linter warnings [5]. Additionally, note that the behavior of setting this value to 0 changed in version 7.0.0: whereas it previously acted as a "disable" flag, it now blocks all requests to the endpoint [1][2]. To disable the rate limiter, you should use the skip function instead [6][2].

Citations:


Switch express-rate-limit from max to limit (and ensure correct client IP behind proxies)

express-rate-limit uses limit as the preferred configuration name; max is kept only for backwards compatibility, so updating avoids future tooling/lint noise and aligns with the current API.

♻️ Proposed change
   windowMs: 15 * 60 * 1000, // 15 minutes
-  max: 20, // 20 attempts per window per IP
+  limit: 20, // 20 attempts per window per IP

Also, if this service is behind a reverse proxy (Nginx/ELB), configure app.set('trust proxy', ...) so rate limiting keys off the real client IP (otherwise it may throttle the proxy IP for everyone).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/routes/auth.routes.js` around lines 8 - 17, Replace the
deprecated/compat `max` option with the current `limit` option in the rateLimit
configuration used to create authLimiter (i.e., change the property on the
rateLimit call that constructs authLimiter). Also ensure your Express app is
configured to respect upstream proxies by setting app.set('trust proxy', ...) in
your app bootstrap/init code so rateLimit keys off the real client IP rather
than the proxy; keep the existing standardHeaders/legacyHeaders/message settings
unchanged.


// 2. Add the login route definition
// This maps to: POST /api/auth/login
router.post('/login', login);
router.post('/register', authLimiter, register);
router.post('/login', authLimiter, login);

module.exports = router;