@gov-cy/govcy-express-services
Version:
An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.
399 lines (340 loc) • 16.5 kB
JavaScript
import express from 'express';
import session from 'express-session';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import cookieParser from 'cookie-parser'; // Required to read cookies
import https from 'https';
import { requestTimer } from './middleware/govcyRequestTimer.mjs';
import { noCacheAndSecurityHeaders } from "./middleware/govcyHeadersControl.mjs";
import { renderGovcyPage } from "./middleware/govcyPageRender.mjs";
import { govcyPageHandler } from './middleware/govcyPageHandler.mjs';
import { govcyFormsPostHandler } from './middleware/govcyFormsPostHandler.mjs';
import { govcyReviewPostHandler } from './middleware/govcyReviewPostHandler.mjs';
import { govcyReviewPageHandler } from './middleware/govcyReviewPageHandler.mjs';
import { govcySuccessPageHandler } from './middleware/govcySuccessPageHandler.mjs';
import { requestLogger } from './middleware/govcyLogger.mjs';
import { govcyCsrfMiddleware } from './middleware/govcyCsrf.mjs';
import { govcySessionData } from './middleware/govcySessionData.mjs';
import { govcyHttpErrorHandler } from './middleware/govcyHttpErrorHandler.mjs';
import { govcyLanguageMiddleware } from './middleware/govcyLanguageMiddleware.mjs';
import { requireAuth, cyLoginPolicy, handleLoginRoute, handleSigninOidc, handleLogout } from './middleware/cyLoginAuth.mjs';
import { serviceConfigDataMiddleware } from './middleware/govcyConfigSiteData.mjs';
import { govcyManifestHandler } from './middleware/govcyManifestHandler.mjs';
import { govcyRoutePageHandler } from './middleware/govcyRoutePageHandler.mjs';
import { govcyServiceEligibilityHandler } from './middleware/govcyServiceEligibilityHandler.mjs';
import { govcyLoadSubmissionData } from './middleware/govcyLoadSubmissionData.mjs';
import { govcyFileUpload } from './middleware/govcyFileUpload.mjs';
import { govcyFileDeletePageHandler, govcyFileDeletePostHandler } from './middleware/govcyFileDeleteHandler.mjs';
import { govcyFileViewHandler } from './middleware/govcyFileViewHandler.mjs';
import { govcyMultipleThingsAddHandler, govcyMultipleThingsEditHandler, govcyMultipleThingsAddPostHandler, govcyMultipleThingsEditPostHandler } from './middleware/govcyMultipleThingsItemPage.mjs';
import { govcyMultipleThingsDeletePageHandler, govcyMultipleThingsDeletePostHandler } from './middleware/govcyMultipleThingsDeleteHandler.mjs';
import { govcyUpdateMyDetailsPostHandler } from './middleware/govcyUpdateMyDetails.mjs';
import { isProdOrStaging, getEnvVariable, whatsIsMyEnvironment } from './utils/govcyEnvVariables.mjs';
import { logger } from "./utils/govcyLogger.mjs";
import fs from 'fs';
export default function initializeGovCyExpressService(opts = {}) {
const app = express();
// Add this line before session middleware
app.set('trust proxy', 1);
// Get the directory name of the current module
const __dirname = dirname(fileURLToPath(import.meta.url));
// Construct the absolute path to local certificate files
logger.debug('Current directory:', __dirname);
logger.debug('Current working directory:', process.cwd());
const certPath = join(process.cwd(), 'server');
// Determine environment settings
const ENV = whatsIsMyEnvironment();
// Set port
const PORT = getEnvVariable('PORT') || 44319;
// Use HTTPS if isProdOrStaging or certificate files exist
const USE_HTTPS = isProdOrStaging() || (fs.existsSync(certPath + '.cert') && fs.existsSync(certPath + '.key'));
// Middleware
// Enable parsing of URL-encoded data (data from HTML form submissions with application/x-www-form-urlencoded encoding)
app.use(express.urlencoded({ extended: true }));
// Enable parsing of JSON request bodies
app.use(express.json());
// Enable session management
app.use(
session({
secret: getEnvVariable('SESSION_SECRET'), // Use environment variable or fallback for dev. To generate a secret, run: `node -e "console.log(require('crypto').randomBytes(32).toString('hex'));"`
resave: false, // Prevents unnecessary session updates
saveUninitialized: false, // Don't save empty sessions
cookie: {
secure: false, // Secure cookies only if HTTPS is used
httpOnly: true, // Prevents XSS attacks
maxAge: 1800000, // Session expires after 30 mins
sameSite: 'lax' // Prevents CSRF by default
}
})
);
// Enable cookie parsing
app.use(cookieParser());
// Apply language middleware
app.use(govcyLanguageMiddleware);
// Add request timing middleware
app.use(requestTimer);
// add csrf middleware
app.use(govcyCsrfMiddleware);
// Enable security headers
app.use(noCacheAndSecurityHeaders);
// 🔒 cyLogin ----------------------------------------
// 🔒 -- ROUTE: Redirect to Login
app.get('/login', handleLoginRoute());
// 🔒 -- ROUTE: Handle login Callback
app.get('/signin-oidc', handleSigninOidc());
// 🔒 -- ROUTE: Handle Logout
app.get('/logout', handleLogout());
//----------------------------------------------------------------------
// 🛠️ Debugging routes -----------------------------------------------------
// 🙍🏻♂️ -- ROUTE: Debugging route Protected Route
// if (!isProdOrStaging()) {
// app.get('/user', requireAuth, cyLoginPolicy, (req, res) => {
// res.send(`
// User name: ${req.session.user.name}
// <br> Sub: ${req.session.user.sub}
// <br> Profile type: ${req.session.user.profile_type}
// <br> Clinent ip: ${req.session.user.client_ip}
// <br> Unique Identifier: ${req.session.user.unique_identifier}
// <br> Email: ${req.session.user.email}
// <br> Id Token: ${req.session.user.id_token}
// <br> Access Token: ${req.session.user.access_token}
// `);
// });
// }
//----------------------------------------------------------------------
// ✅ Ensures session structure exists
app.use(govcySessionData);
// add logger middleware
app.use(requestLogger);
// Construct the absolute path to the public directory
const publicPath = join(__dirname, 'public');
// 🌐 -- ROUTE: Serve static files in the public directory. Route for `/js/`
app.use(express.static(publicPath));
// 🏡 -- ROUTE: handle the route `/`
app.get('/', govcyRoutePageHandler);
// 📝 -- ROUTE: Serve manifest.json dynamically for each site
app.get('/:siteId/manifest.json', serviceConfigDataMiddleware, govcyManifestHandler());
// 🗃️ -- ROUTE: Handle POST requests for file uploads for a page.
app.post('/apis/:siteId/:pageUrl/upload',
serviceConfigDataMiddleware,
requireAuth, // UNCOMMENT
cyLoginPolicy, // UNCOMMENT
govcyServiceEligibilityHandler(true), // UNCOMMENT
govcyFileUpload);
// 🗃️ -- ROUTE: Handle POST requests for file uploads inside multipleThings (add)
app.post('/apis/:siteId/:pageUrl/multiple/add/upload',
serviceConfigDataMiddleware,
requireAuth, // UNCOMMENT
cyLoginPolicy, // UNCOMMENT
govcyServiceEligibilityHandler(true), // UNCOMMENT
govcyFileUpload
);
// ==========================================================
// Custom pages
// ==========================================================
/**
* siteRoute helper:
* Registers a route NOT under /:siteId, but injects req.params.siteId manually.
*/
const siteRoute = (siteId, method, path, ...handlers) => {
if (typeof app[method] !== "function") {
throw new Error(`Unsupported HTTP method: ${method}`);
}
// Middleware to manually inject the siteId param
const injectSiteId = (req, res, next) => {
req.params = req.params || {};
req.params.siteId = siteId;
next();
};
// Chain your standard middlewares AFTER injection
const wrappedHandlers = [
injectSiteId,
serviceConfigDataMiddleware,
requireAuth,
cyLoginPolicy,
govcyServiceEligibilityHandler(),
...handlers,
];
// ✅ Register under a plain path (no /:siteId/)
// e.g. path = "/custom" → registers "/custom" only
app[method](path, ...wrappedHandlers);
};
// Allow custom routes
if (typeof opts.beforeMount === "function") {
opts.beforeMount({ siteRoute, app });
}
// ==========================================================
// 🗃️ -- ROUTE: Handle POST requests for file uploads inside multipleThings (edit)
app.post('/apis/:siteId/:pageUrl/multiple/edit/:index/upload',
serviceConfigDataMiddleware,
requireAuth, // UNCOMMENT
cyLoginPolicy, // UNCOMMENT
govcyServiceEligibilityHandler(true), // UNCOMMENT
govcyFileUpload
);
// View (multipleThings draft)
app.get('/:siteId/:pageUrl/multiple/add/view-file/:elementName',
serviceConfigDataMiddleware,
requireAuth,
cyLoginPolicy,
govcyServiceEligibilityHandler(true),
govcyFileViewHandler());
// ❌🗃️ -- ROUTE: Delete file during multipleThings ADD (before dynamic route)
app.get('/:siteId/:pageUrl/multiple/add/delete-file/:elementName',
serviceConfigDataMiddleware,
requireAuth,
cyLoginPolicy,
govcyServiceEligibilityHandler(),
govcyLoadSubmissionData(),
govcyFileDeletePageHandler(),
renderGovcyPage()
);
// ❌🗃️ -- ROUTE: Delete file during multipleThings EDIT (before dynamic route)
app.get('/:siteId/:pageUrl/multiple/edit/:index/delete-file/:elementName',
serviceConfigDataMiddleware,
requireAuth,
cyLoginPolicy,
govcyServiceEligibilityHandler(),
govcyLoadSubmissionData(),
govcyFileDeletePageHandler(),
renderGovcyPage()
);
// ❌🗃️📥 -- ROUTE: Handle POST requests for delete file in multipleThings ADD
app.post('/:siteId/:pageUrl/multiple/add/delete-file/:elementName',
serviceConfigDataMiddleware,
requireAuth,
cyLoginPolicy,
govcyServiceEligibilityHandler(true),
govcyFileDeletePostHandler()
);
// ❌🗃️📥 -- ROUTE: Handle POST requests for delete file in multipleThings EDIT
app.post('/:siteId/:pageUrl/multiple/edit/:index/delete-file/:elementName',
serviceConfigDataMiddleware,
requireAuth,
cyLoginPolicy,
govcyServiceEligibilityHandler(true),
govcyFileDeletePostHandler()
);
// View (multipleThings edit)
app.get('/:siteId/:pageUrl/multiple/edit/:index/view-file/:elementName',
serviceConfigDataMiddleware,
requireAuth,
cyLoginPolicy,
govcyServiceEligibilityHandler(true),
govcyFileViewHandler());
// 🏠 -- ROUTE: Handle route with only siteId (/:siteId or /:siteId/)
app.get('/:siteId', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(true), govcyLoadSubmissionData(), govcyPageHandler(), renderGovcyPage());
// 👀 -- ROUTE: Add Review Page Route (BEFORE the dynamic route)
app.get('/:siteId/review', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyReviewPageHandler(), renderGovcyPage());
// ✅ -- ROUTE: Add Success Page Route (BEFORE the dynamic route)
app.get('/:siteId/success', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(), govcySuccessPageHandler(), renderGovcyPage());
// 👀🗃️ -- ROUTE: View file (BEFORE the dynamic route)
app.get('/:siteId/:pageUrl/view-file/:elementName', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyFileViewHandler());
// ❌🗃️ -- ROUTE: Delete file (BEFORE the dynamic route)
app.get('/:siteId/:pageUrl/delete-file/:elementName', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyFileDeletePageHandler(), renderGovcyPage());
// ➕ -- ROUTE: Add item page (BEFORE the generic dynamic route)
app.get('/:siteId/:pageUrl/multiple/add',
serviceConfigDataMiddleware,
requireAuth,
cyLoginPolicy,
govcyServiceEligibilityHandler(true),
govcyLoadSubmissionData(),
govcyMultipleThingsAddHandler(),
renderGovcyPage()
);
// ➕ -- ROUTE: Add item POST (BEFORE the generic POST)
app.post('/:siteId/:pageUrl/multiple/add',
serviceConfigDataMiddleware,
requireAuth,
cyLoginPolicy,
govcyServiceEligibilityHandler(true),
govcyMultipleThingsAddPostHandler()
);
// ✏️ -- ROUTE: Edit item page (BEFORE the generic dynamic route)
app.get('/:siteId/:pageUrl/multiple/edit/:index',
serviceConfigDataMiddleware,
requireAuth,
cyLoginPolicy,
govcyServiceEligibilityHandler(true),
govcyLoadSubmissionData(),
govcyMultipleThingsEditHandler(),
renderGovcyPage()
);
// 🗃️ -- ROUTE: Handle POST requests for multipleThings EDIT item
app.post('/:siteId/:pageUrl/multiple/edit/:index',
serviceConfigDataMiddleware,
requireAuth,
cyLoginPolicy,
govcyServiceEligibilityHandler(true),
govcyMultipleThingsEditPostHandler()
);
// ❌🗃️ -- ROUTE: Delete multipleThings item (BEFORE the dynamic route)
app.get('/:siteId/:pageUrl/multiple/delete/:index',
serviceConfigDataMiddleware,
requireAuth,
cyLoginPolicy,
govcyServiceEligibilityHandler(),
govcyLoadSubmissionData(),
govcyMultipleThingsDeletePageHandler(),
renderGovcyPage()
);
// ❌🗃️📥 -- ROUTE: Handle POST requests for delete multipleThings item
app.post('/:siteId/:pageUrl/multiple/delete/:index',
serviceConfigDataMiddleware,
requireAuth,
cyLoginPolicy,
govcyServiceEligibilityHandler(true),
govcyMultipleThingsDeletePostHandler()
);
// ----- `updateMyDetails` handling
// 🔀➡️ -- ROUTE coming from incoming update my details /:siteId/:pageUrl/update-my-details-response
app.post('/:siteId/:pageUrl/update-my-details-response',
serviceConfigDataMiddleware,
requireAuth,
cyLoginPolicy,
govcyServiceEligibilityHandler(true),
govcyUpdateMyDetailsPostHandler());
// ----- `updateMyDetails` handling
// 📝 -- ROUTE: Dynamic route to render pages based on siteId and pageUrl, using govcyPageHandler middleware
app.get('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(true), govcyLoadSubmissionData(), govcyPageHandler(), renderGovcyPage());
// ❌🗃️📥 -- ROUTE: Handle POST requests for delete file
app.post('/:siteId/:pageUrl/delete-file/:elementName', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(true), govcyFileDeletePostHandler());
// 📥 -- ROUTE: Handle POST requests for review page. The `submit` action
app.post('/:siteId/review', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(), govcyReviewPostHandler());
// 👀📥 -- ROUTE: Handle POST requests (Form Submissions) based on siteId and pageUrl, using govcyFormsPostHandler middleware
app.post('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(true), govcyFormsPostHandler());
// post for /:siteId/review
// 🔹 Catch 404 errors (must be after all routes)
app.use((req, res, next) => {
next({ status: 404, message: "Page not found" });
});
// 🔹 Centralized error handling (must be the LAST middleware)
app.use(govcyHttpErrorHandler);
let server = null;
return {
app,
startServer: () => {
// Start Server
if (!isProdOrStaging()) {
const options = {
key: fs.readFileSync(certPath + '.key'),
cert: fs.readFileSync(certPath + '.cert'),
};
server = https.createServer(options, app).listen(PORT, () => {
logger.info(`🔒 Server running at https://localhost:${PORT} (${ENV})`);
});
} else {
server = app.listen(PORT, () => {
logger.info(`⚡ Server running at http://localhost:${PORT} (${ENV})`);
});
}
},
stopServer: () => {
if (server) {
server.close(() => {
logger.info('Server stopped');
});
}
}
};
}