@gov-cy/govcy-express-services 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1157 -0
  3. package/package.json +72 -0
  4. package/src/auth/cyLoginAuth.mjs +123 -0
  5. package/src/index.mjs +188 -0
  6. package/src/middleware/cyLoginAuth.mjs +131 -0
  7. package/src/middleware/govcyConfigSiteData.mjs +38 -0
  8. package/src/middleware/govcyCsrf.mjs +36 -0
  9. package/src/middleware/govcyFormsPostHandler.mjs +83 -0
  10. package/src/middleware/govcyHeadersControl.mjs +22 -0
  11. package/src/middleware/govcyHttpErrorHandler.mjs +63 -0
  12. package/src/middleware/govcyLanguageMiddleware.mjs +19 -0
  13. package/src/middleware/govcyLogger.mjs +15 -0
  14. package/src/middleware/govcyManifestHandler.mjs +46 -0
  15. package/src/middleware/govcyPDFRender.mjs +30 -0
  16. package/src/middleware/govcyPageHandler.mjs +110 -0
  17. package/src/middleware/govcyPageRender.mjs +14 -0
  18. package/src/middleware/govcyRequestTimer.mjs +29 -0
  19. package/src/middleware/govcyReviewPageHandler.mjs +102 -0
  20. package/src/middleware/govcyReviewPostHandler.mjs +147 -0
  21. package/src/middleware/govcyRoutePageHandler.mjs +37 -0
  22. package/src/middleware/govcyServiceEligibilityHandler.mjs +101 -0
  23. package/src/middleware/govcySessionData.mjs +9 -0
  24. package/src/middleware/govcySuccessPageHandler.mjs +112 -0
  25. package/src/public/img/Certificate_A4.svg +30 -0
  26. package/src/public/js/govcyForms.js +21 -0
  27. package/src/resources/govcyResources.mjs +430 -0
  28. package/src/standalone.mjs +7 -0
  29. package/src/utils/govcyApiRequest.mjs +114 -0
  30. package/src/utils/govcyConstants.mjs +4 -0
  31. package/src/utils/govcyDataLayer.mjs +311 -0
  32. package/src/utils/govcyEnvVariables.mjs +45 -0
  33. package/src/utils/govcyFormHandling.mjs +148 -0
  34. package/src/utils/govcyLoadConfigData.mjs +135 -0
  35. package/src/utils/govcyLogger.mjs +30 -0
  36. package/src/utils/govcyNotification.mjs +85 -0
  37. package/src/utils/govcyPdfMaker.mjs +27 -0
  38. package/src/utils/govcyReviewSummary.mjs +205 -0
  39. package/src/utils/govcySubmitData.mjs +530 -0
  40. package/src/utils/govcyUtils.mjs +13 -0
  41. package/src/utils/govcyValidator.mjs +352 -0
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@gov-cy/govcy-express-services",
3
+ "version": "0.1.1",
4
+ "description": "An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.",
5
+ "author": "DMRID - DSF Team",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./src/index.mjs",
9
+ "module": "./src/index.mjs",
10
+ "exports": {
11
+ "import": "./src/index.mjs"
12
+ },
13
+ "files": [
14
+ "src"
15
+ ],
16
+ "keywords": [
17
+ "govcy",
18
+ "cyprus",
19
+ "unified design system",
20
+ "uds",
21
+ "dsf",
22
+ "digital service framework",
23
+ "forms",
24
+ "dynamic-rendering",
25
+ "service",
26
+ "renderer",
27
+ "frontend",
28
+ "express services",
29
+ "express",
30
+ "builder"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/gov-cy/govcy-express-services.git"
35
+ },
36
+ "homepage": "https://github.com/gov-cy/govcy-express-services",
37
+ "scripts": {
38
+ "dev": "nodemon src/standalone.mjs",
39
+ "start": "node src/standalone.mjs",
40
+ "start:mock": "node tests/mocks/mockApiServer.mjs",
41
+ "test": "mocha --timeout 60000 tests/**/*.test.mjs --exit",
42
+ "test:report": "mocha --timeout 60000 --reporter mochawesome tests/**/*.test.mjs --exit",
43
+ "test:unit": "mocha --recursive tests/unit/**/*.test.mjs",
44
+ "test:integration": "mocha --recursive tests/integration/**/*.test.mjs",
45
+ "test:package": "mocha --recursive tests/package/**/*.test.mjs",
46
+ "test:functional": "mocha --timeout 30000 --recursive tests/functional/**/*.test.mjs",
47
+ "test:watch": "mocha --watch --timeout 60000 tests/**/*.test.mjs"
48
+ },
49
+ "dependencies": {
50
+ "@gov-cy/dsf-email-templates": "^2.1.0",
51
+ "@gov-cy/govcy-frontend-renderer": "^1.17.0",
52
+ "axios": "^1.9.0",
53
+ "cookie-parser": "^1.4.7",
54
+ "dotenv": "^16.3.1",
55
+ "express": "^4.18.2",
56
+ "express-session": "^1.17.3",
57
+ "openid-client": "^6.3.4",
58
+ "puppeteer": "^24.6.0"
59
+ },
60
+ "devDependencies": {
61
+ "chai": "^5.2.0",
62
+ "chai-http": "^5.1.1",
63
+ "mocha": "^11.1.0",
64
+ "mochawesome": "^7.1.3",
65
+ "nodemon": "^3.0.2",
66
+ "pa11y": "^8.0.0",
67
+ "sinon": "^20.0.0"
68
+ },
69
+ "engines": {
70
+ "node": ">=18.0.0"
71
+ }
72
+ }
@@ -0,0 +1,123 @@
1
+ import * as client from 'openid-client';
2
+ import { getEnvVariable } from '../utils/govcyEnvVariables.mjs';
3
+ import { logger } from "../utils/govcyLogger.mjs";
4
+
5
+
6
+ // OpenID Configuration
7
+ const issuerUrl = getEnvVariable('CYLOGIN_ISSUER_URL');
8
+ const clientId = getEnvVariable('CYLOGIN_CLIENT_ID');
9
+ const clientSecret = getEnvVariable('CYLOGIN_CLIENT_SECRET');
10
+ const scope = getEnvVariable('CYLOGIN_SCOPE');
11
+ const redirect_uri = getEnvVariable('CYLOGIN_REDIRECT_URI');
12
+
13
+ // Discover OpenID settings with error handling and retry mechanism
14
+ let config = null; // Changed: Initialize config as null
15
+ async function initializeConfig() {
16
+ try {
17
+ config = await client.discovery(new URL(issuerUrl), clientId, clientSecret);
18
+ logger.info('OpenID configuration successfully discovered.');
19
+ } catch (error) {
20
+ logger.error('Failed to discover OpenID configuration:', error);
21
+ logger.debug('Failed to discover OpenID configuration:', issuerUrl, error)
22
+ config = null; // Ensure config remains null if discovery fails
23
+ }
24
+ }
25
+
26
+ // Initial attempt to load config
27
+ await initializeConfig();
28
+
29
+ // Retry mechanism to reinitialize config if it fails
30
+ setInterval(async () => {
31
+ if (!config) {
32
+ logger.debug('Retrying OpenID configuration discovery...');
33
+ await initializeConfig();
34
+ }
35
+ }, 60000); // Retry every 60 seconds (adjust as needed)
36
+
37
+ /**
38
+ * Generate login URL
39
+ */
40
+ export async function getLoginUrl(req) {
41
+ try {
42
+ if (!config) throw new Error('OpenID configuration is unavailable.');
43
+
44
+ let code_verifier = client.randomPKCECodeVerifier(); // Generate random PKCE per request
45
+ let code_challenge = await client.calculatePKCECodeChallenge(code_verifier); // Ensure `await` is here
46
+
47
+ let nonce = client.randomNonce(); // Generate per request
48
+
49
+ // Store these in session
50
+ req.session.pkce = { code_verifier, nonce };
51
+
52
+ let parameters = {
53
+ redirect_uri,
54
+ scope,
55
+ code_challenge,
56
+ code_challenge_method: getEnvVariable('CYLOGIN_CODE_CHALLENGE_METHOD'),
57
+ nonce,
58
+ ui_locales: req.globalLang || 'el', // Default to 'el' if req.globalLang is not set
59
+ };
60
+
61
+ return client.buildAuthorizationUrl(config, parameters).href;
62
+ } catch (error) {
63
+ throw new Error('Unable to generate login URL at this time.');
64
+ }
65
+ }
66
+
67
+
68
+
69
+ /**
70
+ * Handle authorization code and exchange it for tokens
71
+ */
72
+ export async function handleCallback(req) {
73
+ try {
74
+ if (!config) throw new Error('OpenID configuration is unavailable.');
75
+
76
+ let currentUrl = new URL(`${req.protocol}://${req.get('host')}${req.originalUrl}`);
77
+
78
+ let { code_verifier, nonce } = req.session.pkce || {}; // Retrieve from session
79
+
80
+ let tokens = await client.authorizationCodeGrant(config, currentUrl, {
81
+ pkceCodeVerifier: code_verifier, // Validate PKCE
82
+ expectedNonce: nonce, // Validate nonce
83
+ idTokenExpected: true,
84
+ });
85
+
86
+ delete req.session.pkce; // Clear PKCE data after successful login
87
+
88
+ logger.debug('Token Endpoint Response', tokens);
89
+
90
+ let { access_token } = tokens;
91
+ let claims = tokens.claims();
92
+ logger.debug('ID Token Claims', claims);
93
+
94
+ let { sub } = claims;
95
+ let userInfo = await client.fetchUserInfo(config, access_token, sub);
96
+ logger.debug('UserInfo Response', userInfo);
97
+
98
+ return { tokens, claims, userInfo };
99
+ } catch (error) {
100
+ logger.debug('Error processing login callback:', error);
101
+ throw new Error('Unable to process login callback at this time.');
102
+ }
103
+ }
104
+
105
+
106
+ /**
107
+ * Logout and build end session URL
108
+ */
109
+ export function getLogoutUrl(id_token_hint = '') {
110
+ try {
111
+ if (!config) throw new Error('OpenID configuration is unavailable.');
112
+
113
+ return client.buildEndSessionUrl(config, {
114
+ post_logout_redirect_uri: getEnvVariable('CYLOGIN_POST_LOGOUR_REIDRECT_URI'),
115
+ id_token_hint, // Send ID token if available
116
+ }).href;
117
+ } catch (error) {
118
+ throw new Error('Unable to generate logout URL at this time.');
119
+ }
120
+ }
121
+
122
+ // Export config if needed elsewhere
123
+ export { config };
package/src/index.mjs ADDED
@@ -0,0 +1,188 @@
1
+ import express from 'express';
2
+ import session from 'express-session';
3
+ import { dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import cookieParser from 'cookie-parser'; // Required to read cookies
6
+ import https from 'https';
7
+ import { requestTimer } from './middleware/govcyRequestTimer.mjs';
8
+ import { noCacheAndSecurityHeaders } from "./middleware/govcyHeadersControl.mjs";
9
+ import { renderGovcyPage } from "./middleware/govcyPageRender.mjs";
10
+ import { govcyPageHandler } from './middleware/govcyPageHandler.mjs';
11
+ import { govcyPDFRender } from './middleware/govcyPDFRender.mjs';
12
+ import { govcyFormsPostHandler } from './middleware/govcyFormsPostHandler.mjs';
13
+ import { govcyReviewPostHandler } from './middleware/govcyReviewPostHandler.mjs';
14
+ import { govcyReviewPageHandler } from './middleware/govcyReviewPageHandler.mjs';
15
+ import { govcySuccessPageHandler } from './middleware/govcySuccessPageHandler.mjs';
16
+ import { requestLogger } from './middleware/govcyLogger.mjs';
17
+ import { govcyCsrfMiddleware } from './middleware/govcyCsrf.mjs';
18
+ import { govcySessionData } from './middleware/govcySessionData.mjs';
19
+ import { govcyHttpErrorHandler } from './middleware/govcyHttpErrorHandler.mjs';
20
+ import { govcyLanguageMiddleware } from './middleware/govcyLanguageMiddleware.mjs';
21
+ import { requireAuth, naturalPersonPolicy,handleLoginRoute, handleSigninOidc, handleLogout } from './middleware/cyLoginAuth.mjs';
22
+ import { serviceConfigDataMiddleware } from './middleware/govcyConfigSiteData.mjs';
23
+ import { govcyManifestHandler } from './middleware/govcyManifestHandler.mjs';
24
+ import { govcyRoutePageHandler } from './middleware/govcyRoutePageHandler.mjs';
25
+ import { govcyServiceEligibilityHandler } from './middleware/govcyServiceEligibilityHandler.mjs';
26
+ import { isProdOrStaging , getEnvVariable, whatsIsMyEnvironment } from './utils/govcyEnvVariables.mjs';
27
+ import { logger } from "./utils/govcyLogger.mjs";
28
+
29
+ import fs from 'fs';
30
+
31
+ export default function initializeGovCyExpressService(){
32
+ const app = express();
33
+
34
+ // Get the directory name of the current module
35
+ const __dirname = dirname(fileURLToPath(import.meta.url));
36
+ // Construct the absolute path to local certificate files
37
+ logger.debug('Current directory:', __dirname);
38
+ logger.debug('Current working directory:', process.cwd());
39
+ const certPath = join(process.cwd(),'server');
40
+
41
+ // Determine environment settings
42
+ const ENV = whatsIsMyEnvironment();
43
+ // Set port
44
+ const PORT = getEnvVariable('PORT') || 44319;
45
+ // Use HTTPS if isProdOrStaging or certificate files exist
46
+ const USE_HTTPS = isProdOrStaging() || (fs.existsSync(certPath + '.cert') && fs.existsSync(certPath + '.key'));
47
+
48
+
49
+ // Middleware
50
+ // Enable parsing of URL-encoded data (data from HTML form submissions with application/x-www-form-urlencoded encoding)
51
+ app.use(express.urlencoded({ extended: true }));
52
+ // Enable parsing of JSON request bodies
53
+ app.use(express.json());
54
+ // Enable session management
55
+ app.use(
56
+ session({
57
+ 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'));"`
58
+ resave: false, // Prevents unnecessary session updates
59
+ saveUninitialized: false, // Don't save empty sessions
60
+ cookie: {
61
+ secure: USE_HTTPS, // Secure cookies only if HTTPS is used
62
+ httpOnly: true, // Prevents XSS attacks
63
+ maxAge: 1800000, // Session expires after 30 mins
64
+ sameSite: 'lax' // Prevents CSRF by default
65
+ }
66
+ })
67
+ );
68
+ // Enable cookie parsing
69
+ app.use(cookieParser());
70
+ // Apply language middleware
71
+ app.use(govcyLanguageMiddleware);
72
+ // Add request timing middleware
73
+ app.use(requestTimer);
74
+ // add csrf middleware
75
+ app.use(govcyCsrfMiddleware);
76
+ // Enable security headers
77
+ app.use(noCacheAndSecurityHeaders);
78
+
79
+ // ๐Ÿ”’ cyLogin ----------------------------------------
80
+
81
+ // ๐Ÿ”’ -- ROUTE: Redirect to Login
82
+ app.get('/login', handleLoginRoute() );
83
+
84
+ // ๐Ÿ”’ -- ROUTE: Handle login Callback
85
+ app.get('/signin-oidc', handleSigninOidc() );
86
+
87
+ // ๐Ÿ”’ -- ROUTE: Handle Logout
88
+ app.get('/logout', handleLogout() );
89
+
90
+ //----------------------------------------------------------------------
91
+
92
+
93
+ // ๐Ÿ› ๏ธ Debugging routes -----------------------------------------------------
94
+ // ๐Ÿ™๐Ÿปโ€โ™‚๏ธ -- ROUTE: Debugging route Protected Route
95
+ if (!isProdOrStaging()) {
96
+ app.get('/user', requireAuth, naturalPersonPolicy, (req, res) => {
97
+ res.send(`
98
+ User name: ${req.session.user.name}
99
+ <br> Sub: ${req.session.user.sub}
100
+ <br> Profile type: ${req.session.user.profile_type}
101
+ <br> Clinent ip: ${req.session.user.client_ip}
102
+ <br> Unique Identifier: ${req.session.user.unique_identifier}
103
+ <br> Email: ${req.session.user.email}
104
+ <br> Id Token: ${req.session.user.id_token}
105
+ <br> Access Token: ${req.session.user.access_token}
106
+ `);
107
+ });
108
+ }
109
+ //----------------------------------------------------------------------
110
+
111
+
112
+ // โœ… Ensures session structure exists
113
+ app.use(govcySessionData);
114
+ // add logger middleware
115
+ app.use(requestLogger);
116
+
117
+ // Construct the absolute path to the public directory
118
+ const publicPath = join(__dirname, 'public');
119
+ // ๐ŸŒ -- ROUTE: Serve static files in the public directory. Route for `/js/`
120
+ app.use(express.static(publicPath));
121
+
122
+ // ๐Ÿก -- ROUTE: handle the route `/`
123
+ app.get('/', govcyRoutePageHandler);
124
+
125
+ // ๐Ÿ“ -- ROUTE: Serve manifest.json dynamically for each site
126
+ app.get('/:siteId/manifest.json', serviceConfigDataMiddleware, govcyManifestHandler());
127
+
128
+ // ๐Ÿ  -- ROUTE: Handle route with only siteId (/:siteId or /:siteId/)
129
+ app.get('/:siteId', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true),govcyPageHandler(), renderGovcyPage());
130
+
131
+ // ๐Ÿ‘€ -- ROUTE: Add Review Page Route (BEFORE the dynamic route)
132
+ app.get('/:siteId/review',serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyReviewPageHandler(), renderGovcyPage());
133
+
134
+ // โœ…๐Ÿ“„ -- ROUTE: Add Success PDF Route (BEFORE the dynamic route)
135
+ app.get('/:siteId/success/pdf',serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcySuccessPageHandler(true), govcyPDFRender());
136
+
137
+ // โœ… -- ROUTE: Add Success Page Route (BEFORE the dynamic route)
138
+ app.get('/:siteId/success',serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcySuccessPageHandler(), renderGovcyPage());
139
+
140
+ // ๐Ÿ“ -- ROUTE: Dynamic route to render pages based on siteId and pageUrl, using govcyPageHandler middleware
141
+ app.get('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyPageHandler(), renderGovcyPage());
142
+
143
+ // ๐Ÿ“ฅ -- ROUTE: Handle POST requests for review page
144
+ app.post('/:siteId/review', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyReviewPostHandler());
145
+
146
+ // ๐Ÿ‘€๐Ÿ“ฅ -- ROUTE: Handle POST requests (Form Submissions) based on siteId and pageUrl, using govcyFormsPostHandler middleware
147
+ app.post('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyFormsPostHandler());
148
+
149
+
150
+ // post for /:siteId/review
151
+
152
+ // ๐Ÿ”น Catch 404 errors (must be after all routes)
153
+ app.use((req, res, next) => {
154
+ next({ status: 404, message: "Page not found" });
155
+ });
156
+
157
+ // ๐Ÿ”น Centralized error handling (must be the LAST middleware)
158
+ app.use(govcyHttpErrorHandler);
159
+
160
+ let server = null;
161
+
162
+ return {
163
+ app,
164
+ startServer: () => {
165
+ // Start Server
166
+ if (USE_HTTPS) {
167
+ const options = {
168
+ key: fs.readFileSync(certPath + '.key'),
169
+ cert: fs.readFileSync(certPath + '.cert'),
170
+ };
171
+ server = https.createServer(options, app).listen(PORT, () => {
172
+ logger.info(`๐Ÿ”’ Server running at https://localhost:${PORT} (${ENV})`);
173
+ });
174
+ } else {
175
+ server = app.listen(PORT, () => {
176
+ logger.info(`โšก Server running at http://localhost:${PORT} (${ENV})`);
177
+ });
178
+ }
179
+ },
180
+ stopServer: () => {
181
+ if (server) {
182
+ server.close(() => {
183
+ logger.info('Server stopped');
184
+ });
185
+ }
186
+ }
187
+ };
188
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * @fileoverview This module provides middleware functions for authentication and authorization in an Express application.
3
+ * It includes functions to check if a user is logged in, handle login and logout routes, and enforce natural person policy.
4
+ *
5
+ * @module cyLoginAuth
6
+ */
7
+ import { getLoginUrl, handleCallback, getLogoutUrl } from '../auth/cyLoginAuth.mjs';
8
+ import { logger } from "../utils/govcyLogger.mjs";
9
+ import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
10
+
11
+ /**
12
+ * Middleware to check if the user is authenticated. If not, redirect to the login page.
13
+ *
14
+ * @param {object} req The request object
15
+ * @param {object} res The response object
16
+ * @param {object} next The next middleware function
17
+ */
18
+ export function requireAuth(req, res, next) {
19
+ if (!req.session.user) {
20
+ // Store the original URL before redirecting to login
21
+ req.session.redirectAfterLogin = req.originalUrl;
22
+ return res.redirect('/login');
23
+ }
24
+ next();
25
+ }
26
+
27
+ /**
28
+ * Middleware to enforce natural person policy. If the user is not a natural person, return a 403 error.
29
+ *
30
+ * @param {object} req The request object
31
+ * @param {object} res The response object
32
+ * @param {object} next The next middleware function
33
+ */
34
+ export function naturalPersonPolicy(req, res, next) {
35
+ // // allow only natural persons with approved profiles
36
+ // if (req.session.user.profile_type == 'Individual' && req.session.user.unique_identifier) {
37
+ // next();
38
+ // } else {
39
+ // return handleMiddlewareError("๐Ÿšจ Access Denied: natural person policy not met.", 403, next);
40
+ // }
41
+ // https://dev.azure.com/cyprus-gov-cds/Documentation/_wiki/wikis/Documentation/42/For-Cyprus-Natural-or-Legal-person
42
+ const { profile_type, unique_identifier } = req.session.user || {};
43
+ // Allow only natural persons with approved profiles
44
+ if (profile_type === 'Individual' && unique_identifier) {
45
+
46
+ // Validate Cypriot Citizen (starts with "00" and is 10 characters long)
47
+ if (unique_identifier.startsWith('00') && unique_identifier.length === 10) {
48
+ return next();
49
+ }
50
+
51
+ // Validate Foreigner with ARN (starts with "05" and is 10 characters long)
52
+ if (unique_identifier.startsWith('05') && unique_identifier.length === 10) {
53
+ return next();
54
+ }
55
+ }
56
+
57
+ // Deny access if validation fails
58
+ return handleMiddlewareError("๐Ÿšจ Access Denied: natural person policy not met.", 403, next);
59
+ }
60
+
61
+ /**
62
+ * Middleware to handle the login route. Redirects the user to the login URL.
63
+ *
64
+ * @param {object} req The request object
65
+ * @param {object} res The response object
66
+ * @param {object} next The next middleware function
67
+ */
68
+ export function handleLoginRoute() {
69
+ return async (req, res, next) => {
70
+ try {
71
+ let loginUrl = await getLoginUrl(req);
72
+ res.redirect(loginUrl);
73
+ } catch (error) {
74
+ next(error); // Pass any errors to Express error handler
75
+ }
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Middleware to handle the sign-in callback from the authentication provider.
81
+ *
82
+ * @param {object} req The request object
83
+ * @param {object} res The response object
84
+ * @param {object} next The next middleware function
85
+ */
86
+ export function handleSigninOidc() {
87
+ return async (req, res, next) => {
88
+ try {
89
+ const { tokens, claims, userInfo } = await handleCallback(req);
90
+ // Store user information in session
91
+ req.session.user = {
92
+ ...userInfo,
93
+ id_token: tokens.id_token,
94
+ access_token: tokens.access_token,
95
+ refresh_token: tokens.refresh_token || null,
96
+ };
97
+
98
+ // Redirect to the stored URL after login or fallback to '/'
99
+ const redirectUrl = req.session.redirectAfterLogin || '/';
100
+ // Clean up session for redirect after login
101
+ delete req.session.redirectAfterLogin;
102
+ // Redirect to the stored URL
103
+ res.redirect(redirectUrl);
104
+ } catch (error) {
105
+ logger.debug('Token exchange failed:', error,req);
106
+ res.status(500).send('Authentication failed');
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Middleware to handle the logout route. Destroys the session and redirects the user to the logout URL.
113
+ *
114
+ * @param {object} req The request object
115
+ * @param {object} res The response object
116
+ * @param {object} next The next middleware function
117
+ */
118
+ export function handleLogout() {
119
+ return (req, res, next) => {
120
+ if (!req.session.user) {
121
+ return res.redirect('/'); // Redirect if not logged in
122
+ }
123
+
124
+ const id_token_hint = req.session.user.id_token; // Retrieve ID token
125
+
126
+ req.session.destroy(() => {
127
+ const logoutUrl = getLogoutUrl(id_token_hint);
128
+ res.redirect(logoutUrl);
129
+ });
130
+ };
131
+ }
@@ -0,0 +1,38 @@
1
+ import { getServiceConfigData } from "../utils/govcyLoadConfigData.mjs";
2
+
3
+ /**
4
+ * Middleware to load service configuration data based on siteId and language.
5
+ * This middleware fetches the service data and attaches it to the request object.
6
+ *
7
+ * @param {object} req The request object
8
+ * @param {object} res The response object
9
+ * @param {object} next The next middleware function
10
+ */
11
+ export async function serviceConfigDataMiddleware(req, res, next) {
12
+ try {
13
+ const { siteId } = req.params;
14
+ req.serviceData = await getServiceConfigData(siteId, req.globalLang);
15
+
16
+ // Store current service
17
+ if (siteId) {
18
+ //create a cookie for current service
19
+ res.cookie('cs', siteId, {
20
+ maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year
21
+ httpOnly: true,
22
+ sameSite: 'lax'
23
+ });
24
+ // req.session.homeRedirectPage = req.serviceData.site.homeRedirectPage;
25
+ } else {
26
+ // delete the cookie if id is not available
27
+ res.clearCookie('cs', {
28
+ httpOnly: true,
29
+ sameSite: 'lax'
30
+ });
31
+ }
32
+
33
+ next();
34
+ } catch (error) {
35
+ return next(error)
36
+ }
37
+
38
+ }
@@ -0,0 +1,36 @@
1
+
2
+ import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
3
+ /**
4
+ * Middleware to handle CSRF token generation and validation.
5
+ *
6
+ * @param {object} req The request object
7
+ * @param {object} res The response object
8
+ * @param {object} next The next middleware function
9
+ */
10
+ export function govcyCsrfMiddleware(req, res, next) {
11
+ // Generate token on first request per session
12
+ if (!req.session.csrfToken) {
13
+ req.session.csrfToken = generateRandonToken();
14
+ }
15
+
16
+ req.csrfToken = () => req.session.csrfToken;
17
+
18
+ // Check token on POST requests
19
+ if (req.method === 'POST') {
20
+ const tokenFromBody = req.body._csrf;
21
+ if (!tokenFromBody || tokenFromBody !== req.session.csrfToken) {
22
+ return handleMiddlewareError("๐Ÿšจ Invalid CSRF token", 403, next); // Pass error to govcyHttpErrorHandler
23
+ }
24
+ }
25
+
26
+ next();
27
+ }
28
+
29
+ /**
30
+ * Generate a random token string.
31
+ *
32
+ * @returns {string} A random token string
33
+ */
34
+ export function generateRandonToken() {
35
+ return [...Array(32)].map(() => Math.random().toString(36)[2]).join('');
36
+ }
@@ -0,0 +1,83 @@
1
+ import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
2
+ import * as govcyResources from "../resources/govcyResources.mjs";
3
+ import { validateFormElements } from "../utils/govcyValidator.mjs"; // Import your validator
4
+ import * as dataLayer from "../utils/govcyDataLayer.mjs";
5
+ import { logger } from "../utils/govcyLogger.mjs";
6
+ import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
7
+ import { getFormData } from "../utils/govcyFormHandling.mjs"
8
+
9
+ /**
10
+ * Middleware to handle page form submission
11
+ */
12
+ export function govcyFormsPostHandler() {
13
+ return (req, res, next) => {
14
+ try {
15
+ const { siteId, pageUrl } = req.params;
16
+
17
+ // โคต๏ธ Load service and check if it exists
18
+ const service = req.serviceData;
19
+
20
+ // โคต๏ธ Find the current page based on the URL
21
+ const page = getPageConfigData(service, pageUrl);
22
+
23
+ // ๐Ÿ” Find the form definition inside `pageTemplate.sections`
24
+ let formElement = null;
25
+ for (const section of page.pageTemplate.sections) {
26
+ formElement = section.elements.find(el => el.element === "form");
27
+ if (formElement) break;
28
+ }
29
+
30
+ if (!formElement) {
31
+ return handleMiddlewareError("๐Ÿšจ Form definition not found.", 500, next);
32
+ }
33
+
34
+ // const formData = req.body; // Submitted data
35
+ const formData = getFormData(formElement.params.elements, req.body); // Submitted data
36
+
37
+ // โ˜‘๏ธ Start validation from top-level form elements
38
+ const validationErrors = validateFormElements(formElement.params.elements, formData);
39
+
40
+ // โŒ Return validation errors if any exist
41
+ if (Object.keys(validationErrors).length > 0) {
42
+ logger.debug("๐Ÿšจ Validation errors:", validationErrors, req);
43
+ logger.info("๐Ÿšจ Validation errors on:", req.originalUrl);
44
+ // store the validation errors
45
+ dataLayer.storePageValidationErrors(req.session, siteId, pageUrl, validationErrors, formData);
46
+ //redirect to the same page with error summary
47
+ return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
48
+ }
49
+
50
+ //โคด๏ธ Store validated form data in session
51
+ dataLayer.storePageData(req.session, siteId, pageUrl, formData);
52
+
53
+
54
+ logger.debug("โœ… Form submitted successfully:", dataLayer.getPageData(req.session, siteId, pageUrl), req);
55
+ logger.info("โœ… Form submitted successfully:", req.originalUrl);
56
+
57
+ // ๐Ÿ” Determine next page (if applicable)
58
+ let nextPage = null;
59
+ for (const section of page.pageTemplate.sections) {
60
+ const form = section.elements.find(el => el.element === "form");
61
+ if (form) {
62
+ //handle review route
63
+ if (req.query.route === "review") {
64
+ nextPage = govcyResources.constructPageUrl(siteId, "review");
65
+ } else {
66
+ nextPage = page.pageData.nextPage;
67
+ //nextPage = form.params.elements.find(el => el.element === "button" && el.params?.prototypeNavigate)?.params.prototypeNavigate;
68
+ }
69
+ }
70
+ }
71
+
72
+ // โžก๏ธ Redirect to the next page if defined, otherwise return success
73
+ if (nextPage) {
74
+ logger.debug("๐Ÿ”„ Redirecting to next page:", nextPage, req);
75
+ // ๐Ÿ›  Fix relative paths
76
+ return res.redirect(govcyResources.constructPageUrl(siteId, `${nextPage.split('/').pop()}`));
77
+ }
78
+ res.json({ success: true, message: "Form submitted successfully" });
79
+ } catch (error) {
80
+ return next(error); // Pass error to govcyHttpErrorHandler
81
+ }
82
+ };
83
+ }