@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.
- package/LICENSE +21 -0
- package/README.md +1157 -0
- package/package.json +72 -0
- package/src/auth/cyLoginAuth.mjs +123 -0
- package/src/index.mjs +188 -0
- package/src/middleware/cyLoginAuth.mjs +131 -0
- package/src/middleware/govcyConfigSiteData.mjs +38 -0
- package/src/middleware/govcyCsrf.mjs +36 -0
- package/src/middleware/govcyFormsPostHandler.mjs +83 -0
- package/src/middleware/govcyHeadersControl.mjs +22 -0
- package/src/middleware/govcyHttpErrorHandler.mjs +63 -0
- package/src/middleware/govcyLanguageMiddleware.mjs +19 -0
- package/src/middleware/govcyLogger.mjs +15 -0
- package/src/middleware/govcyManifestHandler.mjs +46 -0
- package/src/middleware/govcyPDFRender.mjs +30 -0
- package/src/middleware/govcyPageHandler.mjs +110 -0
- package/src/middleware/govcyPageRender.mjs +14 -0
- package/src/middleware/govcyRequestTimer.mjs +29 -0
- package/src/middleware/govcyReviewPageHandler.mjs +102 -0
- package/src/middleware/govcyReviewPostHandler.mjs +147 -0
- package/src/middleware/govcyRoutePageHandler.mjs +37 -0
- package/src/middleware/govcyServiceEligibilityHandler.mjs +101 -0
- package/src/middleware/govcySessionData.mjs +9 -0
- package/src/middleware/govcySuccessPageHandler.mjs +112 -0
- package/src/public/img/Certificate_A4.svg +30 -0
- package/src/public/js/govcyForms.js +21 -0
- package/src/resources/govcyResources.mjs +430 -0
- package/src/standalone.mjs +7 -0
- package/src/utils/govcyApiRequest.mjs +114 -0
- package/src/utils/govcyConstants.mjs +4 -0
- package/src/utils/govcyDataLayer.mjs +311 -0
- package/src/utils/govcyEnvVariables.mjs +45 -0
- package/src/utils/govcyFormHandling.mjs +148 -0
- package/src/utils/govcyLoadConfigData.mjs +135 -0
- package/src/utils/govcyLogger.mjs +30 -0
- package/src/utils/govcyNotification.mjs +85 -0
- package/src/utils/govcyPdfMaker.mjs +27 -0
- package/src/utils/govcyReviewSummary.mjs +205 -0
- package/src/utils/govcySubmitData.mjs +530 -0
- package/src/utils/govcyUtils.mjs +13 -0
- 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
|
+
}
|