qgenutils
Version:
A security-first Node.js utility library providing authentication, HTTP operations, URL processing, validation, datetime formatting, and template rendering. Designed as a lightweight alternative to heavy npm packages with comprehensive error handling and
210 lines (202 loc) • 9.92 kB
JavaScript
/*
* WHY THIS MODULE EXISTS:
* - Standardize EJS rendering with graceful fallback when templates fail.
* - Reduce boilerplate when creating simple template routes.
*
* MAIN PROBLEMS SOLVED:
* - Prevents crashes from template errors by sending user friendly pages.
* - Simplifies registration of routes that only render views.
*
* EXPRESS ASSUMPTIONS:
* - An Express app using EJS is available (global `app`).
* - Response objects support status(), send() and render().
*/
/*
* View Rendering Utility Module
*
* This module provides utilities for rendering EJS templates with robust error
* handling and route registration. It focuses on graceful degradation when
* template rendering fails and providing helpful error messages to users.
*
* DESIGN PHILOSOPHY:
* - User-friendly errors: Never show users cryptic template error messages
* - Debugging support: Log detailed error information for developers
* - Graceful fallback: Provide helpful error pages instead of crashes
* - Developer experience: Centralize template rendering logic for consistency
*
* EJS INTEGRATION:
* EJS (Embedded JavaScript) is a popular templating engine for Express.js.
* Template rendering can fail for various reasons, and this module handles
* those failures gracefully while maintaining good user experience.
*
* ERROR PAGE STRATEGY SUMMARY:
* - Sanitize error messages before including them in HTML output to prevent
* injection attacks while still surfacing useful debug information
* - Log detailed errors with qerrors so developers have full context
* - Return a minimal HTML page with navigation back to the home page so users
* are never left on a blank or cryptic screen
*/
const { qerrors } = require('qerrors'); // structured error logging
const logger = require('./logger'); // structured logger
const { hasMethod } = require('./input-validation'); // utility for checking methods
/**
* Render EJS template with graceful error handling
*
* RATIONALE: Template rendering can fail for various reasons (missing templates,
* syntax errors, data issues). Rather than letting these errors crash the server
* or show cryptic error pages, this function provides graceful fallback with
* helpful error messages.
*
* The function serves multiple purposes:
* 1. Centralized error handling for all view rendering
* 2. User-friendly error pages that explain what went wrong
* 3. Comprehensive error logging for debugging template issues
* 4. Fallback navigation to prevent users from getting stuck
*
* This is especially important for admin pages where template errors could
* prevent access to critical functionality.
*
* IMPLEMENTATION DECISIONS:
* - Try template rendering first, handle errors in catch block
* - Send HTML error page (not JSON) since this is for user-facing pages
* - Include error message in page for debugging (be careful in production)
* - Provide "Return to Home" link for user navigation
* - Use 500 status code to indicate server error
*
* ERROR PAGE DESIGN:
* The error page includes:
* - Clear heading indicating the specific page that failed
* - Technical error message (useful for developers)
* - Navigation link to prevent users from being stuck
* - Simple HTML that doesn't rely on CSS/JS that might also be broken
*
* SECURITY CONSIDERATIONS:
* - Error messages might reveal server file paths or internal structure
* - In production, consider showing generic error messages to users
* - Detailed errors should still be logged for debugging purposes
*
* ALTERNATIVE APPROACHES CONSIDERED:
* - Redirect to error page - rejected because it loses context
* - JSON error response - rejected because this is for HTML pages
* - Generic error page - rejected because specific context is valuable
*
* @param {Object} res - Express response object for rendering templates
* @param {string} viewName - Name of the EJS template to render (e.g., 'dashboard', 'login')
* @param {string} errorTitle - Human-readable title for error page if rendering fails
*/
function renderView(res, viewName, errorTitle) {
console.log(`renderView is running with ${viewName}`); logger.debug(`renderView is running with ${viewName}`); // Log rendering attempt for debugging template issues
try {
// Validate parameters
if (!hasMethod(res, 'render')) {
console.log(`renderView: Invalid res object provided`); logger.debug(`renderView: Invalid res object provided`);
return null;
}
// Attempt to render the specified view/template
// This will throw an error if the template doesn't exist or has syntax issues
res.render(viewName);
} catch (err) {
// Handle template rendering errors gracefully without crashing the request
// Log error with template context for debugging template issues
qerrors(err, 'renderView', {viewName}); // centralized error log retains stack without leaking details
console.error(`Error rendering ${viewName}:`, err); // Additional console logging for immediate debugging
// Send user-friendly error page with helpful information if res is available
// This prevents users from seeing cryptic error messages or blank pages
if (hasMethod(res, 'status') && hasMethod(res, 'send')) { // ensure error page methods exist
const sanitizedMessage = err.message
.replace(/&/g, '&') // escape ampersand so entities display safely and cannot inject
.replace(/</g, '<') // escape opening angle bracket to neutralize tags and prevent XSS
.replace(/>/g, '>'); // escape closing angle bracket for same reason
res.status(500).send(`
<h1>${errorTitle}</h1>
<p>There was an error rendering the ${viewName} page:</p>
<pre>${sanitizedMessage}</pre>
<p>Please contact support if this error persists.</p>
<p><a href="/">Return to Home</a></p>
`); // include navigation link for user convenience
}
}
}
/**
* Attach view route handler to Express app
*
* RATIONALE: Many routes simply render a template without complex logic.
* This function reduces boilerplate by creating route handlers that just
* render templates with error handling built in.
*
* IMPLEMENTATION STRATEGY:
* - Use global 'app' object (assumes Express app is globally available)
* - Create route handler that calls renderView with appropriate parameters
* - Handle route registration errors gracefully
* - Log route registration for debugging routing issues
*
* USE CASES:
* - Static pages (about, contact, help)
* - Admin dashboard pages
* - Simple form pages without complex logic
* - Landing pages that just need template rendering
*
* DESIGN ASSUMPTIONS:
* - Express app is available as global variable 'app'
* - Routes are simple GET requests that just render templates
* - No middleware or complex logic needed for these routes
* - Error handling follows the same pattern as renderView
*
* GLOBAL APP PATTERN:
* Using a global 'app' variable is a common pattern in smaller Express applications.
* For larger applications, consider dependency injection or module imports.
*
* ALTERNATIVE APPROACHES CONSIDERED:
* - Pass app as parameter - rejected for simplicity in common use case
* - Return route handler function - rejected because caller still needs app
* - Middleware pattern - rejected because these are specific endpoints
*
* @param {string} routePath - The route path to register (e.g., '/dashboard', '/admin')
* @param {string} viewName - Name of the EJS template to render
* @param {string} title - Title for error page if rendering fails
*/
function registerViewRoute(route, viewName, errorTitle) {
console.log(`registerViewRoute is running with ${route}`); logger.debug(`registerViewRoute is running with ${route}`); // Log route registration for debugging
try {
// Check if global app object exists and has the get method
// We use global.app so the main server can register routes in one place without passing app around
if (typeof global !== 'undefined' && hasMethod(global.app, 'get')) {
// Register GET route with the provided path
// The handler function will be called when the route is accessed
global.app.get(route, (req, res) => {
// Use the renderView function to handle template rendering with error recovery
renderView(res, viewName, errorTitle);
});
console.log(`registerViewRoute successfully registered ${route}`); logger.debug(`registerViewRoute successfully registered ${route}`);
} else {
console.log(`registerViewRoute: global.app not available for ${route}`); logger.debug(`registerViewRoute: global.app not available for ${route}`);
}
} catch (error) {
// Handle any errors during route registration (invalid route patterns, etc.)
qerrors(error, 'registerViewRoute', { route, viewName, errorTitle }); // maintain traceability for route setup failures
console.log(`registerViewRoute failed for ${route}`); logger.debug(`registerViewRoute failed for ${route}`);
}
}
/*
* Module Export Strategy:
*
* We export both functions because they serve complementary purposes:
*
* 1. renderView - Direct template rendering with error handling (for use in existing routes)
* 2. registerViewRoute - Automated route creation for simple template pages
*
* This gives developers flexibility to either:
* - Use renderView in existing route handlers with custom logic
* - Use registerViewRoute to quickly create simple template-only routes
*
* FUTURE ENHANCEMENTS:
* - Add support for template data injection
* - Add support for middleware in registerViewRoute
* - Add support for multiple HTTP methods
* - Add support for custom error page templates
* - Add caching for frequently rendered templates
*/
module.exports = {
renderView, // export template renderer
registerViewRoute // export Express route helper
};