Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/barcode8/VideoHub/llms.txt

Use this file to discover all available pages before exploring further.

VideoHub’s security model is built around three interlocking mechanisms: CORS origin allowlisting to prevent unauthorized browser clients from reaching the API, HttpOnly cookies to transport tokens safely without exposing them to JavaScript, and signed JWTs to authenticate every protected request in a stateless fashion. Understanding how each layer is configured — and how to harden it for production — is essential before deploying the backend publicly.

CORS configuration

Cross-Origin Resource Sharing is configured in src/app.js immediately after the Express app is created. The allowed origin is read from the CORS_ORIGIN environment variable, so no code changes are required when switching between environments.
app.use(cors({
    origin: process.env.CORS_ORIGIN,
    credentials: true
}))
The two important settings here are:
  • origin — only requests from the specified origin are allowed. Any request from a different origin will be rejected by the browser’s preflight check before it reaches any route handler.
  • credentials: true — instructs the browser to include cookies (and Authorization headers) on cross-origin requests. Without this flag, the browser strips cookies from every cross-origin fetch, which would break authentication entirely.

Supporting multiple origins in production

The current configuration accepts exactly one origin string. If your production deployment serves multiple frontend domains (for example a primary domain and a staging subdomain), you need to update app.js to accept an array and match against it dynamically:
const ALLOWED_ORIGINS = process.env.CORS_ORIGIN.split(",").map(o => o.trim());

app.use(cors({
    origin: function (origin, callback) {
        if (!origin || ALLOWED_ORIGINS.includes(origin)) {
            callback(null, true);
        } else {
            callback(new Error(`CORS: origin '${origin}' is not allowed`));
        }
    },
    credentials: true
}))
Set CORS_ORIGIN to a comma-separated list of domains: https://videohub.app,https://staging.videohub.app.
Access tokens and refresh tokens are delivered to the client as HttpOnly cookies, not as JSON body values that get saved to localStorage. This is the primary defense against cross-site scripting (XSS) attacks stealing authentication credentials. The cookie options object used in the loginUser controller (src/controllers/user.controller.js) is:
const options = {
    httpOnly: true,
    secure: false,
    sameSite: "Lax"
}

return res
    .status(200)
    .cookie("accessToken", accessToken, options)
    .cookie("refreshToken", refreshToken, options)
    .json(...)
FlagValue (dev)Effect
httpOnlytrueThe cookie is inaccessible to document.cookie and any JavaScript running in the browser.
securefalseThe cookie is sent over both HTTP and HTTPS. Must be set to true in production.
sameSite"Lax"The cookie is sent on same-site requests and top-level cross-site navigations, but not on cross-site sub-resource requests. This provides a reasonable balance between usability and CSRF protection.
The logoutUser controller (which clears cookies) and the refreshAccessToken controller (which reissues token pairs) both use a stricter options object that omits sameSite and enables secure:
const options = {
    httpOnly: true,
    secure: true
}
These two controllers therefore already enforce HTTPS-only cookie transmission. Align the login cookie options to match before going to production.
In production, set secure: true on the login cookie options and ensure the server is running behind HTTPS (either directly or via a reverse proxy such as nginx or a load balancer). A cookie with secure: false will be transmitted over plain HTTP, where it can be captured by a network attacker.

Request size limits

The middleware stack in src/app.js applies explicit size constraints to all incoming request bodies: JSON payloads
app.use(express.json({
    limit: "16kb"
}))
Any request with a Content-Type: application/json body larger than 16 kilobytes is rejected with a 413 Payload Too Large response before reaching a route handler. This prevents large JSON blobs from consuming server memory. URL-encoded form data
app.use(express.urlencoded({
    extended: true
}))
URL-encoded bodies (HTML form submissions) are parsed with the qs library (extended: true), which supports nested objects and arrays. No explicit size cap is set here beyond the default Express limits. File uploads File uploads are handled by Multer (src/middlewares/multer.middleware.js), which writes incoming files to a temporary directory at public/temp/ before the upload controller sends them to Cloudinary:
export const upload = multer({
    storage: storage,
    limits: {
        fileSize: 500 * 1024 * 1024   // 500 MB per file
    }
})
Multer enforces a 500 MB per-file ceiling at the Node.js layer. Files are stored temporarily on disk under public/temp/ with a unique timestamped filename, then uploaded to Cloudinary and deleted from disk. Note that Cloudinary enforces its own limits on video duration and file size based on your account plan.

JWT security

JSON Web Tokens are the backbone of VideoHub’s stateless authentication. Both token types are generated in src/models/user.models.js using jsonwebtoken: Access tokens are short-lived and carry the user’s _id, email, username, and fullname as claims. They expire after the duration set in ACCESS_TOKEN_EXPIRY (default: 1d). Refresh tokens carry only the user’s _id and have a longer lifetime controlled by REFRESH_TOKEN_EXPIRY (default: 10d). Crucially, the current refresh token is stored in the user’s database document. On logout, the logoutUser controller sets refreshToken to undefined, which immediately invalidates the token server-side even before it expires:
await User.findByIdAndUpdate(
    req.user._id,
    { $set: { refreshToken: undefined } },
    { new: true }
)

The verifyJwt middleware

Every protected route passes through the verifyJwt middleware defined in src/middlewares/auth.middleware.js. It checks for a token in two places — cookies first, then the Authorization header — so the API works for both browser clients (cookies) and programmatic clients (bearer tokens):
const token = req.cookies?.accessToken
    || req.header("Authorization")?.replace("Bearer ", "")

if (!token) {
    throw new ApiError(401, "Please login first to perform this action")
}

const decodedToken = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET)

const user = await User.findById(decodedToken?._id).select("-password -refreshToken")

if (!user) {
    throw new ApiError(401, "Invalid access token")
}

req.user = user;
next()
If the token is missing, forged, or expired, the middleware throws a 401 Unauthorized error. The password and refreshToken fields are explicitly excluded from the database query so they are never attached to req.user and cannot accidentally leak into response payloads. A second middleware, verifyJWTIfAvailable, follows the same lookup pattern but silently continues on failure — it is used on public routes (such as video browsing) where authentication is optional but enriches the response if a valid token is present.
Production security checklistBefore going live, verify all of the following:
  • HTTPS is enforced — run the server behind a TLS-terminating reverse proxy (nginx, Caddy) or deploy to a platform that provides HTTPS automatically.
  • CORS_ORIGIN is set to the exact frontend domain — wildcards (*) must never be used alongside credentials: true.
  • secure: true is set on all cookie options in the login and refresh token controllers.
  • JWT secrets are long and random — at least 64 bytes of entropy. Generate them with node -e "console.log(require('crypto').randomBytes(64).toString('hex'))".
  • JWT secrets are rotated periodically — existing sessions will be invalidated, which forces users to log in again; schedule rotations during low-traffic windows.
  • ACCESS_TOKEN_EXPIRY is short — prefer 15m or 1h in production and rely on the refresh flow to maintain long-lived sessions.

Build docs developers (and LLMs) love