ready to test

Testing

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-06-22 10:06:58 +08:00
parent bf48c95467
commit 439bb33f77
189 changed files with 17559 additions and 686 deletions
+39 -1
View File
@@ -45,4 +45,42 @@ const authenticate = async (req, res, next) => {
}
};
module.exports = { authenticate };
/**
* Soft authentication for endpoints that are useful both authenticated and unauthenticated.
*
* Behaviour:
* - No Authorization header → guest mode (req.user = null, continues)
* - Valid token → authenticated (req.user = user, continues)
* - Present but invalid/expired token → 401 (a token was attempted; reject it)
*
* The distinction between "no token" and "bad token" matters:
* silently ignoring a malformed token would allow it to be used as a scapegoat
* to probe the endpoint without being flagged as unauthenticated.
*/
const softAuthenticate = async (req, res, next) => {
const authHeader = req.headers.authorization;
// No token at all → guest mode
if (!authHeader?.startsWith('Bearer ')) {
req.user = null;
return next();
}
// Token present → validate it fully (same rules as authenticate)
try {
const token = authHeader.split(' ')[1];
const decoded = verifyAccessToken(token);
const user = await mdl_Users.findByPk(decoded.user_id, {
attributes: { exclude: ['password', 'otp_code', 'otp_expires_at'] },
});
req.user = (user && user.is_active) ? user : null;
next();
} catch (err) {
if (err.name === 'TokenExpiredError')
return R.error(res, 'Token expired. Please log in again.', 401);
return R.error(res, 'Invalid token.', 401);
}
};
module.exports = { authenticate, softAuthenticate };
+45
View File
@@ -0,0 +1,45 @@
/***********************************************************************************************************************************************************************
* File Name: avatar_upload.middleware.js
* Type of Program: Middleware
* Description: Multer config for avatar uploads.
* Memory storage — buffer is passed directly to S3.
* Accepts JPEG, PNG, WebP, GIF only. 5 MB limit.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 18, 2026
***********************************************************************************************************************************************************************/
'use strict';
const multer = require('multer');
const ALLOWED_MIME = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB
fileFilter(req, file, cb) {
if (ALLOWED_MIME.has(file.mimetype)) return cb(null, true);
cb(Object.assign(new Error('Only JPEG, PNG, WebP, or GIF images are allowed.'), { code: 'INVALID_TYPE' }));
},
});
const avatarSingle = upload.single('avatar');
// Wraps multer errors into the project's standard error shape.
const handleAvatarUpload = (req, res, next) => {
avatarSingle(req, res, (err) => {
if (!err) return next();
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ status: 'error', message: 'Avatar must be under 5 MB.' });
}
if (err.code === 'INVALID_TYPE') {
return res.status(400).json({ status: 'error', message: err.message });
}
console.error('[MULTER AVATAR]', err);
return res.status(400).json({ status: 'error', message: err.message ?? 'Upload failed.' });
});
};
module.exports = { handleAvatarUpload };
+50
View File
@@ -0,0 +1,50 @@
/***********************************************************************************************************************************************************************
* File Name: mediaGuard.middleware.js
* Type of Program: Middleware
* Description: Blocks non-browser clients from accessing protected media stream endpoints.
* Checks:
* 1. User-Agent blocklist — cURL, wget, python, httpie, etc.
* 2. Origin / Referer — must be present (browser always sends at least one)
* 3. Allowed origin — must match APP_ORIGIN env (prevents hotlinking)
* Author: rgrgogu
* Date Created: Jun. 12, 2026
***********************************************************************************************************************************************************************/
"use strict";
const R = require("../utils/response.util");
const UA_BLOCKLIST = [
"curl", "wget", "python-requests", "python-urllib",
"axios", "httpie", "insomnia", "postman", "thunder client",
"go-http-client", "java/", "ruby", "php/", "perl/",
];
const APP_ORIGIN = process.env.FRONTEND_URL ?? "http://localhost:5173";
module.exports = function mediaGuard(req, res, next) {
const ua = (req.headers["user-agent"] ?? "").toLowerCase();
const origin = req.headers["origin"] ?? "";
const referer = req.headers["referer"] ?? "";
// ── 1. Block known non-browser user-agents ────────────────────────────────
if (UA_BLOCKLIST.some((b) => ua.includes(b))) {
return R.error(res, "Forbidden.", 403);
}
// ── 2. No UA at all → almost certainly a script ──────────────────────────
if (!ua) {
return R.error(res, "Forbidden.", 403);
}
// ── 3. Must have at least Origin or Referer (browsers always send one) ───
if (!origin && !referer) {
return R.error(res, "Forbidden.", 403);
}
// ── 4. Origin must match app origin (blocks hotlinking from other sites) ─
if (origin && origin !== APP_ORIGIN) {
return R.error(res, "Forbidden.", 403);
}
next();
};
+80
View File
@@ -0,0 +1,80 @@
/***********************************************************************************************************************************************************************
* File Name: originGuard.middleware.js
* Type of Program: Middleware
* Description: Two-layer server-side guard that blocks non-browser clients from reaching any API route.
*
* ── Layer 1 — Sec-Fetch-Site (ALL methods including GET) ────────────────────────────────────────
*
* Browsers (Chrome 76+, Firefox 90+, Safari 16.4+) automatically attach the Sec-Fetch-Site
* header on every request. It is a forbidden request header — JavaScript cannot set, override,
* or remove it. Its absence is a reliable, low-spoofability signal that the request originated
* from a tool rather than a real browser.
*
* Tools blocked by this layer (default configurations):
* ✓ Metasploit (Rex HTTP client) — no Sec-Fetch-Site
* ✓ BurpSuite Repeater / Scanner — no Sec-Fetch-Site
* ✓ Postman — no Sec-Fetch-Site
* ✓ curl / wget / httpie / python-requests — no Sec-Fetch-Site
* ✓ Nikto — no Sec-Fetch-Site
* ✓ sqlmap — no Sec-Fetch-Site
* ✓ dirb / gobuster / feroxbuster — no Sec-Fetch-Site
* ✓ nmap HTTP scripts — no Sec-Fetch-Site
*
* ── Layer 2 — Origin allowlist (POST / PUT / PATCH / DELETE only) ───────────────────────────────
*
* State-mutating requests must also carry an Origin header that matches ALLOWED_ORIGINS.
* Stops credential-stuffing and cross-origin mutation attempts from unlisted domains,
* even if an attacker managed to set Sec-Fetch-Site manually.
*
* ── What this does NOT stop ──────────────────────────────────────────────────────────────────────
*
* ✗ BurpSuite running as MITM proxy through a real browser session.
* The browser supplies all correct headers — requests are indistinguishable from
* legitimate traffic. The only defences here are rate limiting and valid credentials.
* ✗ A determined attacker who manually replicates all browser headers in their tool.
*
* ── Browser compatibility note ───────────────────────────────────────────────────────────────────
*
* Sec-Fetch-Site is supported by Chrome 76+ (Aug 2019), Firefox 90+ (Jul 2021),
* and Safari 16.4+ (Mar 2023). Requests from browsers older than these thresholds
* will be rejected. Given this is a modern SPA (Vite + React), this is acceptable.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 20, 2026
* Date Modified: Jun. 20, 2026
***********************************************************************************************************************************************************************/
"use strict";
const R = require('../utils/response.util');
const ALLOWED = (process.env.ALLOWED_ORIGINS || process.env.APP_URL || '')
.split(',')
.map(o => o.trim())
.filter(Boolean);
const MUTATION_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
module.exports = function originGuard(req, res, next) {
// Dev bypass: set ORIGIN_GUARD_DISABLED=true in .env to allow Postman/curl through.
// Hardcoded production lock — never bypassed even if the flag is accidentally set.
if (process.env.ORIGIN_GUARD_DISABLED === 'true' && process.env.NODE_ENV !== 'production') {
return next();
}
if (req.method === 'OPTIONS') return next(); // preflight — handled by cors()
// ── Layer 1: Sec-Fetch-Site must be present (covers GET scanning) ─────────
if (!req.headers['sec-fetch-site']) {
return R.error(res, 'Forbidden.', 403);
}
// ── Layer 2: Origin must be in allowlist for state-changing requests ──────
if (MUTATION_METHODS.has(req.method)) {
const origin = req.headers['origin'];
if (!origin || !ALLOWED.includes(origin)) {
return R.error(res, 'Forbidden.', 403);
}
}
next();
};
+30 -2
View File
@@ -6,23 +6,41 @@
* - authLimiter → applied to login/register (20 req / 15 min)
* - otpLimiter → applied to OTP send/verify (5 req / 15 min)
* - sensitiveOpsLimiter → password change, account delete (10 req / hour)
* - adminLimiter → admin routes (200 req / 15 min)
*
* Store selection is driven by CACHE_DRIVER:
* redis → RedisStore (shared across processes; required in production)
* memory → in-process MemoryStore (fine for local dev, single process)
* Author: rgrgogu
* Date Created: Oct. 6, 2025
* Date Modified: Jun. 19, 2026
***********************************************************************************************************************************************************************
* HOW TO USE:
* const { authLimiter } = require('../middleware/rateLimiter.middleware');
* router.post('/login', authLimiter, loginHandler);
***********************************************************************************************************************************************************************/
const rateLimit = require('express-rate-limit');
const redisClient = require('../config/redis.config');
const windowMs15 = 15 * 60 * 1000; // 15 minutes
const makeStore = redisClient
? (() => {
const { RedisStore } = require('rate-limit-redis');
return (prefix) => new RedisStore({
sendCommand: (...args) => redisClient.sendCommand(args),
prefix: `rl:${prefix}:`,
});
})()
: () => undefined; // undefined → express-rate-limit uses its default MemoryStore
/** Applied globally in server.js */
const globalLimiter = rateLimit({
windowMs: windowMs15,
max: 1000,
standardHeaders: true,
legacyHeaders: false,
store: makeStore('global'),
message: { status: 'error', message: 'Too many requests, please try again later.' },
});
@@ -32,6 +50,7 @@ const authLimiter = rateLimit({
max: 20,
standardHeaders: true,
legacyHeaders: false,
store: makeStore('auth'),
message: { status: 'error', message: 'Too many auth attempts. Please wait 15 minutes.' },
});
@@ -41,15 +60,23 @@ const otpLimiter = rateLimit({
max: 5,
standardHeaders: true,
legacyHeaders: false,
store: makeStore('otp'),
message: { status: 'error', message: 'Too many OTP requests. Please wait 15 minutes.' },
});
/** Password change, account delete */
/**
* Sensitive write operations — bulk actions, uploads, financial transactions.
* Keys by authenticated user ID when available, falls back to IP.
* Keying by IP alone is unfair on shared networks (office NAT, university Wi-Fi)
* where one user triggering the limit would block everyone behind the same IP.
*/
const sensitiveOpsLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10,
keyGenerator: (req) => req.user?.user_id?.toString() ?? req.ip,
standardHeaders: true,
legacyHeaders: false,
store: makeStore('sensitive'),
message: { status: 'error', message: 'Too many sensitive operations. Please wait 1 hour.' },
});
@@ -59,7 +86,8 @@ const adminLimiter = rateLimit({
max: 200,
standardHeaders: true,
legacyHeaders: false,
store: makeStore('admin'),
message: { status: 'error', message: 'Too many admin requests. Please wait 15 minutes.' },
});
module.exports = { globalLimiter, authLimiter, otpLimiter, sensitiveOpsLimiter, adminLimiter };
module.exports = { globalLimiter, authLimiter, otpLimiter, sensitiveOpsLimiter, adminLimiter };
+50
View File
@@ -0,0 +1,50 @@
/***********************************************************************************************************************************************************************
* File Name: taskUpload.middleware.js
* Type of Program: Middleware
* Description: Multer config for task completion file uploads.
* Uses memory storage — buffer is passed directly to S3.
* Single file per request (matches the two-step upload flow in
* ViewTaskDetails where each file is uploaded individually).
*
* Limits:
* fileSize: 500 MB (matches FileUpload.jsx DEFAULT_MAX_BYTES)
*
* Author: rgrgogu
* Date Created: Jun. 13, 2026
***********************************************************************************************************************************************************************/
const multer = require('multer');
const storage = multer.memoryStorage();
const upload = multer({
storage,
limits: {
fileSize: 500 * 1024 * 1024, // 500 MB
},
});
// Single file per request — field name must be "file"
const uploadTaskFile = upload.single('file');
// ─── Error wrapper ────────────────────────────────────────────────────────────
// Converts multer errors to a consistent R.error-style response.
const handleUpload = (req, res, next) => {
uploadTaskFile(req, res, (err) => {
if (!err) return next();
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
status: 'error',
message: 'File exceeds the 500 MB size limit.',
});
}
console.error('[MULTER ERROR]', err);
return res.status(400).json({
status: 'error',
message: err.message ?? 'File upload failed.',
});
});
};
module.exports = { handleUpload };