From 774058439cec1c31621825e9de524ef286b10203 Mon Sep 17 00:00:00 2001 From: GTPSHAX Date: Sun, 17 May 2026 11:25:09 +0700 Subject: [PATCH 01/10] fix: add environment variable checks for hCaptcha and Cloudflare Captcha validation --- packages/handlers/handle-generate-docx.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/handlers/handle-generate-docx.js b/packages/handlers/handle-generate-docx.js index 6aed508..042f5ea 100644 --- a/packages/handlers/handle-generate-docx.js +++ b/packages/handlers/handle-generate-docx.js @@ -130,10 +130,10 @@ const handleGenerateDocx = async (body, req) => { const remoteip = req.ip || req.headers['x-forwarded-for'] || req.socket.remoteAddress // Validate hCaptcha response before proceeding with document generation - if (!await handleHCaptchaValidation(body)) { + if (process.env.HCAPTCHA_SECRET_KEY && !await handleHCaptchaValidation(body)) { return { status: 400, message: 'hCaptcha validation failed' } } - if (!await handleCloudflareCaptchaValidation(body, remoteip)) { + if (process.env.CLOUDFLARE_TURNSTILE_SECRET_KEY && !await handleCloudflareCaptchaValidation(body, remoteip)) { return { status: 400, message: 'Cloudflare Captcha validation failed' } } From 3b1ec6472941bb1608cd4c72fc638e91ddeb3b60 Mon Sep 17 00:00:00 2001 From: GTPSHAX Date: Sun, 17 May 2026 11:35:00 +0700 Subject: [PATCH 02/10] fix: reorder hCaptcha validation to ensure proper execution flow --- packages/handlers/handle-generate-docx.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/handlers/handle-generate-docx.js b/packages/handlers/handle-generate-docx.js index 042f5ea..c48663f 100644 --- a/packages/handlers/handle-generate-docx.js +++ b/packages/handlers/handle-generate-docx.js @@ -130,11 +130,10 @@ const handleGenerateDocx = async (body, req) => { const remoteip = req.ip || req.headers['x-forwarded-for'] || req.socket.remoteAddress // Validate hCaptcha response before proceeding with document generation - if (process.env.HCAPTCHA_SECRET_KEY && !await handleHCaptchaValidation(body)) { - return { status: 400, message: 'hCaptcha validation failed' } - } if (process.env.CLOUDFLARE_TURNSTILE_SECRET_KEY && !await handleCloudflareCaptchaValidation(body, remoteip)) { return { status: 400, message: 'Cloudflare Captcha validation failed' } + } else if (process.env.HCAPTCHA_SECRET_KEY && !await handleHCaptchaValidation(body)) { + return { status: 400, message: 'hCaptcha validation failed' } } const template = 'original' // TODO: Make this dynamic based on request parameter if multiple templates are supported in the future' From bcd1770abd3341e33c52b8c2bf7472de5a672f30 Mon Sep 17 00:00:00 2001 From: GTPSHAX Date: Sun, 17 May 2026 11:46:34 +0700 Subject: [PATCH 03/10] fix: streamline captcha validation logic in document generation --- packages/handlers/handle-generate-docx.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/handlers/handle-generate-docx.js b/packages/handlers/handle-generate-docx.js index c48663f..de00f94 100644 --- a/packages/handlers/handle-generate-docx.js +++ b/packages/handlers/handle-generate-docx.js @@ -129,11 +129,14 @@ function buildRencanaKegiatan (body, kegiatanKeys) { const handleGenerateDocx = async (body, req) => { const remoteip = req.ip || req.headers['x-forwarded-for'] || req.socket.remoteAddress - // Validate hCaptcha response before proceeding with document generation - if (process.env.CLOUDFLARE_TURNSTILE_SECRET_KEY && !await handleCloudflareCaptchaValidation(body, remoteip)) { - return { status: 400, message: 'Cloudflare Captcha validation failed' } - } else if (process.env.HCAPTCHA_SECRET_KEY && !await handleHCaptchaValidation(body)) { - return { status: 400, message: 'hCaptcha validation failed' } + // Validate captcha before proceeding with document generation + const captchaPassed = + await handleCloudflareCaptchaValidation(body, remoteip) || + await handleHCaptchaValidation(body) || + (!process.env.HCAPTCHA_SECRET_KEY && !process.env.CLOUDFLARE_API_TOKEN) + + if (!captchaPassed) { + return { status: 400, message: 'Captcha validation failed' } } const template = 'original' // TODO: Make this dynamic based on request parameter if multiple templates are supported in the future' From 1ad45e455dc874231de9322e4bc872c990be4fe7 Mon Sep 17 00:00:00 2001 From: GTPSHAX Date: Sun, 17 May 2026 12:19:30 +0700 Subject: [PATCH 04/10] fix: refactor CORS trusted hosts parsing and update content security policy directives --- apps/api/middleware/helmet.js | 24 ++++++++++++++------- apps/web/middleware/helmet.js | 39 ++++++++++++++--------------------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/apps/api/middleware/helmet.js b/apps/api/middleware/helmet.js index 20f407e..729862a 100644 --- a/apps/api/middleware/helmet.js +++ b/apps/api/middleware/helmet.js @@ -1,24 +1,32 @@ import helmet from 'helmet' -const CORS_TRUSTED_HOSTS = process.env.CORS_TRUSTED_HOSTS ? process.env.CORS_TRUSTED_HOSTS.split(',').map(host => host.trim()) : [] -const CORS_TRUSTED_CDN_HOSTS = process.env.CORS_TRUSTED_CDN_HOSTS - ? process.env.CORS_TRUSTED_CDN_HOSTS.split(',').map(host => host.trim()) - : [] +/** @returns {string[]} */ +const parseEnvHosts = (/** @type {string} */ key) => + process.env[key]?.split(',').map(h => h.trim()) ?? [] + +const CORS_TRUSTED_HOSTS = parseEnvHosts('CORS_TRUSTED_HOSTS') +const CORS_TRUSTED_CDN_HOSTS = parseEnvHosts('CORS_TRUSTED_CDN_HOSTS') const isDevelopment = process.env.NODE_ENV !== 'production' +const defaultDirectives = helmet.contentSecurityPolicy.getDefaultDirectives() +if (isDevelopment) delete defaultDirectives['upgrade-insecure-requests'] + const helmetMiddleware = helmet({ contentSecurityPolicy: { directives: { - ...helmet.contentSecurityPolicy.getDefaultDirectives(), - - 'upgrade-insecure-requests': isDevelopment ? null : [], + ...defaultDirectives, 'frame-ancestors': ["'self'", ...CORS_TRUSTED_HOSTS], 'frame-src': ["'self'", 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_HOSTS], 'script-src': ["'self'", "'unsafe-inline'", 'https://static.cloudflareinsights.com', 'https://js.hcaptcha.com', 'https://newassets.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_CDN_HOSTS], 'style-src': ["'self'", "'unsafe-inline'", 'https://newassets.hcaptcha.com', ...CORS_TRUSTED_CDN_HOSTS], 'img-src': ["'self'", 'data:', 'https://contrib.rocks', ...CORS_TRUSTED_CDN_HOSTS], 'font-src': ["'self'", ...CORS_TRUSTED_CDN_HOSTS], - 'connect-src': ["'self'", 'https://hcaptcha.com', 'https://*.hcaptcha.com', 'https://challenges.cloudflare.com'] + 'connect-src': [ + "'self'", + 'https://hcaptcha.com', 'https://*.hcaptcha.com', + 'https://challenges.cloudflare.com', + 'https://pat-issuer.cloudflare.com' + ] } }, frameguard: false, diff --git a/apps/web/middleware/helmet.js b/apps/web/middleware/helmet.js index 1f2c4a2..729862a 100644 --- a/apps/web/middleware/helmet.js +++ b/apps/web/middleware/helmet.js @@ -1,41 +1,32 @@ import helmet from 'helmet' -const CORS_TRUSTED_HOSTS = process.env.CORS_TRUSTED_HOSTS ? process.env.CORS_TRUSTED_HOSTS.split(',').map(host => host.trim()) : [] -const CORS_TRUSTED_CDN_HOSTS = process.env.CORS_TRUSTED_CDN_HOSTS - ? process.env.CORS_TRUSTED_CDN_HOSTS.split(',').map(host => host.trim()) - : [] -const APP_API_BASE_URL = process.env.APP_API_BASE_URL || null +/** @returns {string[]} */ +const parseEnvHosts = (/** @type {string} */ key) => + process.env[key]?.split(',').map(h => h.trim()) ?? [] + +const CORS_TRUSTED_HOSTS = parseEnvHosts('CORS_TRUSTED_HOSTS') +const CORS_TRUSTED_CDN_HOSTS = parseEnvHosts('CORS_TRUSTED_CDN_HOSTS') const isDevelopment = process.env.NODE_ENV !== 'production' -/** - * Helper function to remove path from a URL, leaving only the origin (scheme + host + port). - * @param {string} url - * @returns {string} - */ -const removePathFromUrl = (url) => { - try { - const parsedUrl = new URL(url) - parsedUrl.pathname = '' - return parsedUrl.toString().replace(/\/+$/, '') // Remove trailing slash if any - } catch (error) { - console.warn(`Invalid URL provided: ${url}`) - return url - } -} +const defaultDirectives = helmet.contentSecurityPolicy.getDefaultDirectives() +if (isDevelopment) delete defaultDirectives['upgrade-insecure-requests'] const helmetMiddleware = helmet({ contentSecurityPolicy: { directives: { - ...helmet.contentSecurityPolicy.getDefaultDirectives(), - - 'upgrade-insecure-requests': isDevelopment ? null : [], + ...defaultDirectives, 'frame-ancestors': ["'self'", ...CORS_TRUSTED_HOSTS], 'frame-src': ["'self'", 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_HOSTS], 'script-src': ["'self'", "'unsafe-inline'", 'https://static.cloudflareinsights.com', 'https://js.hcaptcha.com', 'https://newassets.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_CDN_HOSTS], 'style-src': ["'self'", "'unsafe-inline'", 'https://newassets.hcaptcha.com', ...CORS_TRUSTED_CDN_HOSTS], 'img-src': ["'self'", 'data:', 'https://contrib.rocks', ...CORS_TRUSTED_CDN_HOSTS], 'font-src': ["'self'", ...CORS_TRUSTED_CDN_HOSTS], - 'connect-src': ["'self'", 'https://hcaptcha.com', 'https://*.hcaptcha.com', 'https://challenges.cloudflare.com', ...(APP_API_BASE_URL ? [removePathFromUrl(APP_API_BASE_URL)] : [])] + 'connect-src': [ + "'self'", + 'https://hcaptcha.com', 'https://*.hcaptcha.com', + 'https://challenges.cloudflare.com', + 'https://pat-issuer.cloudflare.com' + ] } }, frameguard: false, From 9f30b35073a72cc9131c290c1411d0adf9bc53de Mon Sep 17 00:00:00 2001 From: GTPSHAX Date: Sun, 17 May 2026 12:38:49 +0700 Subject: [PATCH 05/10] fix: enhance content security policy directives for improved security and analytics integration --- apps/api/middleware/helmet.js | 36 ++++++++++++++++++++++++++++++----- apps/web/middleware/helmet.js | 36 ++++++++++++++++++++++++++++++----- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/apps/api/middleware/helmet.js b/apps/api/middleware/helmet.js index 729862a..1287cb1 100644 --- a/apps/api/middleware/helmet.js +++ b/apps/api/middleware/helmet.js @@ -16,16 +16,42 @@ const helmetMiddleware = helmet({ directives: { ...defaultDirectives, 'frame-ancestors': ["'self'", ...CORS_TRUSTED_HOSTS], - 'frame-src': ["'self'", 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_HOSTS], - 'script-src': ["'self'", "'unsafe-inline'", 'https://static.cloudflareinsights.com', 'https://js.hcaptcha.com', 'https://newassets.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_CDN_HOSTS], - 'style-src': ["'self'", "'unsafe-inline'", 'https://newassets.hcaptcha.com', ...CORS_TRUSTED_CDN_HOSTS], - 'img-src': ["'self'", 'data:', 'https://contrib.rocks', ...CORS_TRUSTED_CDN_HOSTS], + 'frame-src': [ + "'self'", + 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', + 'https://challenges.cloudflare.com', + ...CORS_TRUSTED_HOSTS + ], + 'script-src': [ + "'self'", "'unsafe-inline'", + 'https://static.cloudflareinsights.com', + 'https://js.hcaptcha.com', 'https://newassets.hcaptcha.com', + 'https://challenges.cloudflare.com', + 'https://www.googletagmanager.com', + 'https://www.google-analytics.com', + ...CORS_TRUSTED_CDN_HOSTS + ], + 'style-src': [ + "'self'", "'unsafe-inline'", + 'https://newassets.hcaptcha.com', + ...CORS_TRUSTED_CDN_HOSTS + ], + 'img-src': [ + "'self'", 'data:', + 'https://contrib.rocks', + 'https://www.google-analytics.com', + 'https://www.googletagmanager.com', + ...CORS_TRUSTED_CDN_HOSTS + ], 'font-src': ["'self'", ...CORS_TRUSTED_CDN_HOSTS], 'connect-src': [ "'self'", 'https://hcaptcha.com', 'https://*.hcaptcha.com', 'https://challenges.cloudflare.com', - 'https://pat-issuer.cloudflare.com' + 'https://pat-issuer.cloudflare.com', + 'https://analytics.google.com', + 'https://stats.g.doubleclick.net', + 'https://*.google-analytics.com' ] } }, diff --git a/apps/web/middleware/helmet.js b/apps/web/middleware/helmet.js index 729862a..1287cb1 100644 --- a/apps/web/middleware/helmet.js +++ b/apps/web/middleware/helmet.js @@ -16,16 +16,42 @@ const helmetMiddleware = helmet({ directives: { ...defaultDirectives, 'frame-ancestors': ["'self'", ...CORS_TRUSTED_HOSTS], - 'frame-src': ["'self'", 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_HOSTS], - 'script-src': ["'self'", "'unsafe-inline'", 'https://static.cloudflareinsights.com', 'https://js.hcaptcha.com', 'https://newassets.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_CDN_HOSTS], - 'style-src': ["'self'", "'unsafe-inline'", 'https://newassets.hcaptcha.com', ...CORS_TRUSTED_CDN_HOSTS], - 'img-src': ["'self'", 'data:', 'https://contrib.rocks', ...CORS_TRUSTED_CDN_HOSTS], + 'frame-src': [ + "'self'", + 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', + 'https://challenges.cloudflare.com', + ...CORS_TRUSTED_HOSTS + ], + 'script-src': [ + "'self'", "'unsafe-inline'", + 'https://static.cloudflareinsights.com', + 'https://js.hcaptcha.com', 'https://newassets.hcaptcha.com', + 'https://challenges.cloudflare.com', + 'https://www.googletagmanager.com', + 'https://www.google-analytics.com', + ...CORS_TRUSTED_CDN_HOSTS + ], + 'style-src': [ + "'self'", "'unsafe-inline'", + 'https://newassets.hcaptcha.com', + ...CORS_TRUSTED_CDN_HOSTS + ], + 'img-src': [ + "'self'", 'data:', + 'https://contrib.rocks', + 'https://www.google-analytics.com', + 'https://www.googletagmanager.com', + ...CORS_TRUSTED_CDN_HOSTS + ], 'font-src': ["'self'", ...CORS_TRUSTED_CDN_HOSTS], 'connect-src': [ "'self'", 'https://hcaptcha.com', 'https://*.hcaptcha.com', 'https://challenges.cloudflare.com', - 'https://pat-issuer.cloudflare.com' + 'https://pat-issuer.cloudflare.com', + 'https://analytics.google.com', + 'https://stats.g.doubleclick.net', + 'https://*.google-analytics.com' ] } }, From 8e92503f0c4a5ae11041cff6570808dadb1a9b67 Mon Sep 17 00:00:00 2001 From: GTPSHAX Date: Sun, 17 May 2026 12:42:12 +0700 Subject: [PATCH 06/10] fix: add Google Analytics to content security policy directives --- apps/api/middleware/helmet.js | 1 + apps/web/middleware/helmet.js | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/api/middleware/helmet.js b/apps/api/middleware/helmet.js index 1287cb1..b52a7eb 100644 --- a/apps/api/middleware/helmet.js +++ b/apps/api/middleware/helmet.js @@ -49,6 +49,7 @@ const helmetMiddleware = helmet({ 'https://hcaptcha.com', 'https://*.hcaptcha.com', 'https://challenges.cloudflare.com', 'https://pat-issuer.cloudflare.com', + 'https://www.google-analytics.com', 'https://analytics.google.com', 'https://stats.g.doubleclick.net', 'https://*.google-analytics.com' diff --git a/apps/web/middleware/helmet.js b/apps/web/middleware/helmet.js index 1287cb1..b52a7eb 100644 --- a/apps/web/middleware/helmet.js +++ b/apps/web/middleware/helmet.js @@ -49,6 +49,7 @@ const helmetMiddleware = helmet({ 'https://hcaptcha.com', 'https://*.hcaptcha.com', 'https://challenges.cloudflare.com', 'https://pat-issuer.cloudflare.com', + 'https://www.google-analytics.com', 'https://analytics.google.com', 'https://stats.g.doubleclick.net', 'https://*.google-analytics.com' From d3d7a3304bea9f6bb201e442ba1436cf863e5682 Mon Sep 17 00:00:00 2001 From: GTPSHAX Date: Sun, 17 May 2026 13:23:10 +0700 Subject: [PATCH 07/10] fix: refactor host parsing logic for content security policy directives --- apps/api/middleware/helmet.js | 26 ++++++++++++++++---------- apps/web/middleware/helmet.js | 26 ++++++++++++++++---------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/apps/api/middleware/helmet.js b/apps/api/middleware/helmet.js index b52a7eb..55ed090 100644 --- a/apps/api/middleware/helmet.js +++ b/apps/api/middleware/helmet.js @@ -1,11 +1,17 @@ import helmet from 'helmet' /** @returns {string[]} */ -const parseEnvHosts = (/** @type {string} */ key) => - process.env[key]?.split(',').map(h => h.trim()) ?? [] -const CORS_TRUSTED_HOSTS = parseEnvHosts('CORS_TRUSTED_HOSTS') -const CORS_TRUSTED_CDN_HOSTS = parseEnvHosts('CORS_TRUSTED_CDN_HOSTS') +/** + * Splits a comma-separated string of hosts into an array of trimmed host strings. + * @param {string} hosts - A comma-separated string of hostnames (e.g., "example.com, api.example.com") + * @returns {string[]} An array of trimmed hostnames (e.g., ["example.com", "api.example.com"]) + */ +const splitHosts = (hosts) => { + if (!hosts) return [] + return hosts.split(',').map(h => h.trim()) +} + const isDevelopment = process.env.NODE_ENV !== 'production' const defaultDirectives = helmet.contentSecurityPolicy.getDefaultDirectives() @@ -15,12 +21,12 @@ const helmetMiddleware = helmet({ contentSecurityPolicy: { directives: { ...defaultDirectives, - 'frame-ancestors': ["'self'", ...CORS_TRUSTED_HOSTS], + 'frame-ancestors': ["'self'", ...splitHosts(process.env.CORS_TRUSTED_HOSTS || '')], 'frame-src': [ "'self'", 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', 'https://challenges.cloudflare.com', - ...CORS_TRUSTED_HOSTS + ...splitHosts(process.env.CORS_TRUSTED_HOSTS || '') ], 'script-src': [ "'self'", "'unsafe-inline'", @@ -29,21 +35,21 @@ const helmetMiddleware = helmet({ 'https://challenges.cloudflare.com', 'https://www.googletagmanager.com', 'https://www.google-analytics.com', - ...CORS_TRUSTED_CDN_HOSTS + ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '') ], 'style-src': [ "'self'", "'unsafe-inline'", 'https://newassets.hcaptcha.com', - ...CORS_TRUSTED_CDN_HOSTS + ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '') ], 'img-src': [ "'self'", 'data:', 'https://contrib.rocks', 'https://www.google-analytics.com', 'https://www.googletagmanager.com', - ...CORS_TRUSTED_CDN_HOSTS + ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '') ], - 'font-src': ["'self'", ...CORS_TRUSTED_CDN_HOSTS], + 'font-src': ["'self'", ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '')], 'connect-src': [ "'self'", 'https://hcaptcha.com', 'https://*.hcaptcha.com', diff --git a/apps/web/middleware/helmet.js b/apps/web/middleware/helmet.js index b52a7eb..55ed090 100644 --- a/apps/web/middleware/helmet.js +++ b/apps/web/middleware/helmet.js @@ -1,11 +1,17 @@ import helmet from 'helmet' /** @returns {string[]} */ -const parseEnvHosts = (/** @type {string} */ key) => - process.env[key]?.split(',').map(h => h.trim()) ?? [] -const CORS_TRUSTED_HOSTS = parseEnvHosts('CORS_TRUSTED_HOSTS') -const CORS_TRUSTED_CDN_HOSTS = parseEnvHosts('CORS_TRUSTED_CDN_HOSTS') +/** + * Splits a comma-separated string of hosts into an array of trimmed host strings. + * @param {string} hosts - A comma-separated string of hostnames (e.g., "example.com, api.example.com") + * @returns {string[]} An array of trimmed hostnames (e.g., ["example.com", "api.example.com"]) + */ +const splitHosts = (hosts) => { + if (!hosts) return [] + return hosts.split(',').map(h => h.trim()) +} + const isDevelopment = process.env.NODE_ENV !== 'production' const defaultDirectives = helmet.contentSecurityPolicy.getDefaultDirectives() @@ -15,12 +21,12 @@ const helmetMiddleware = helmet({ contentSecurityPolicy: { directives: { ...defaultDirectives, - 'frame-ancestors': ["'self'", ...CORS_TRUSTED_HOSTS], + 'frame-ancestors': ["'self'", ...splitHosts(process.env.CORS_TRUSTED_HOSTS || '')], 'frame-src': [ "'self'", 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', 'https://challenges.cloudflare.com', - ...CORS_TRUSTED_HOSTS + ...splitHosts(process.env.CORS_TRUSTED_HOSTS || '') ], 'script-src': [ "'self'", "'unsafe-inline'", @@ -29,21 +35,21 @@ const helmetMiddleware = helmet({ 'https://challenges.cloudflare.com', 'https://www.googletagmanager.com', 'https://www.google-analytics.com', - ...CORS_TRUSTED_CDN_HOSTS + ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '') ], 'style-src': [ "'self'", "'unsafe-inline'", 'https://newassets.hcaptcha.com', - ...CORS_TRUSTED_CDN_HOSTS + ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '') ], 'img-src': [ "'self'", 'data:', 'https://contrib.rocks', 'https://www.google-analytics.com', 'https://www.googletagmanager.com', - ...CORS_TRUSTED_CDN_HOSTS + ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '') ], - 'font-src': ["'self'", ...CORS_TRUSTED_CDN_HOSTS], + 'font-src': ["'self'", ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '')], 'connect-src': [ "'self'", 'https://hcaptcha.com', 'https://*.hcaptcha.com', From 0e49af08a7747a18c0597292e6af3e93840fb7bb Mon Sep 17 00:00:00 2001 From: GTPSHAX Date: Sun, 17 May 2026 13:37:04 +0700 Subject: [PATCH 08/10] fix: remove deprecated autofill-ai and generate-docx routes --- apps/web/routes/api/autofill-ai.js | 22 ---------------------- apps/web/routes/api/generate-docx.js | 28 ---------------------------- 2 files changed, 50 deletions(-) delete mode 100644 apps/web/routes/api/autofill-ai.js delete mode 100644 apps/web/routes/api/generate-docx.js diff --git a/apps/web/routes/api/autofill-ai.js b/apps/web/routes/api/autofill-ai.js deleted file mode 100644 index dd2d111..0000000 --- a/apps/web/routes/api/autofill-ai.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Handling the /autofill-ai endpoint for generating AI-powered autofill suggestions for lesson plan fields - */ - -import { AppRoute } from '../index.js' -import consola from 'consola' -import handleAutoFillAI from '@repo/handlers/handle-autofill-ai.js' - -export const route = new AppRoute('/autofill-ai', 'post', async (req, res) => { - try { - const result = await handleAutoFillAI(req.body) - - if (result.status === 200) { - res.json({ status: true, data: result.data }) - } else { - res.status(result.status).json({ status: false, message: result.message }) - } - } catch (error) { - consola.error('Error generating recommendation:', error) - res.status(500).json({ status: false, message: 'Failed to generate recommendation' }) - } -}) diff --git a/apps/web/routes/api/generate-docx.js b/apps/web/routes/api/generate-docx.js deleted file mode 100644 index f82eaa6..0000000 --- a/apps/web/routes/api/generate-docx.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * This file is handling req for generating the document based on the provided data from the client. - */ - -import { AppRoute } from '../index.js' -import consola from 'consola' -import handleGenerateDocx from '@repo/handlers/handle-generate-docx.js' - -export const route = new AppRoute('/generate-docx', 'post', async (req, res) => { - const body = req.body - - try { - const result = await handleGenerateDocx(body) - - if (result.status === 200) { - const docxBuffer = result.data - res.setHeader('Content-Disposition', 'attachment; filename="Modul_Ajar_' + body.namaSekolah.replace(/\s+/g, '_') + '.docx"') - res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') - return res.status(200).send(docxBuffer) - } else { - consola.error('Failed to generate document:', result.message) - return res.status(500).json({ status: false, message: result.message }) - } - } catch (err) { - consola.error('Error in /generate-docx route:', err) - return res.status(500).json({ status: false, message: 'Internal server error' }) - } -}) From 6f8877cae5f3dd0de0c1bf615684d8e53c4f98b0 Mon Sep 17 00:00:00 2001 From: GTPSHAX Date: Sun, 17 May 2026 13:42:03 +0700 Subject: [PATCH 09/10] fix: refactor content security policy directives for improved host handling --- apps/api/middleware/helmet.js | 69 +++++++---------------------------- apps/web/middleware/helmet.js | 69 +++++++---------------------------- 2 files changed, 28 insertions(+), 110 deletions(-) diff --git a/apps/api/middleware/helmet.js b/apps/api/middleware/helmet.js index 55ed090..20f407e 100644 --- a/apps/api/middleware/helmet.js +++ b/apps/api/middleware/helmet.js @@ -1,65 +1,24 @@ import helmet from 'helmet' -/** @returns {string[]} */ - -/** - * Splits a comma-separated string of hosts into an array of trimmed host strings. - * @param {string} hosts - A comma-separated string of hostnames (e.g., "example.com, api.example.com") - * @returns {string[]} An array of trimmed hostnames (e.g., ["example.com", "api.example.com"]) - */ -const splitHosts = (hosts) => { - if (!hosts) return [] - return hosts.split(',').map(h => h.trim()) -} - +const CORS_TRUSTED_HOSTS = process.env.CORS_TRUSTED_HOSTS ? process.env.CORS_TRUSTED_HOSTS.split(',').map(host => host.trim()) : [] +const CORS_TRUSTED_CDN_HOSTS = process.env.CORS_TRUSTED_CDN_HOSTS + ? process.env.CORS_TRUSTED_CDN_HOSTS.split(',').map(host => host.trim()) + : [] const isDevelopment = process.env.NODE_ENV !== 'production' -const defaultDirectives = helmet.contentSecurityPolicy.getDefaultDirectives() -if (isDevelopment) delete defaultDirectives['upgrade-insecure-requests'] - const helmetMiddleware = helmet({ contentSecurityPolicy: { directives: { - ...defaultDirectives, - 'frame-ancestors': ["'self'", ...splitHosts(process.env.CORS_TRUSTED_HOSTS || '')], - 'frame-src': [ - "'self'", - 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', - 'https://challenges.cloudflare.com', - ...splitHosts(process.env.CORS_TRUSTED_HOSTS || '') - ], - 'script-src': [ - "'self'", "'unsafe-inline'", - 'https://static.cloudflareinsights.com', - 'https://js.hcaptcha.com', 'https://newassets.hcaptcha.com', - 'https://challenges.cloudflare.com', - 'https://www.googletagmanager.com', - 'https://www.google-analytics.com', - ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '') - ], - 'style-src': [ - "'self'", "'unsafe-inline'", - 'https://newassets.hcaptcha.com', - ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '') - ], - 'img-src': [ - "'self'", 'data:', - 'https://contrib.rocks', - 'https://www.google-analytics.com', - 'https://www.googletagmanager.com', - ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '') - ], - 'font-src': ["'self'", ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '')], - 'connect-src': [ - "'self'", - 'https://hcaptcha.com', 'https://*.hcaptcha.com', - 'https://challenges.cloudflare.com', - 'https://pat-issuer.cloudflare.com', - 'https://www.google-analytics.com', - 'https://analytics.google.com', - 'https://stats.g.doubleclick.net', - 'https://*.google-analytics.com' - ] + ...helmet.contentSecurityPolicy.getDefaultDirectives(), + + 'upgrade-insecure-requests': isDevelopment ? null : [], + 'frame-ancestors': ["'self'", ...CORS_TRUSTED_HOSTS], + 'frame-src': ["'self'", 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_HOSTS], + 'script-src': ["'self'", "'unsafe-inline'", 'https://static.cloudflareinsights.com', 'https://js.hcaptcha.com', 'https://newassets.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_CDN_HOSTS], + 'style-src': ["'self'", "'unsafe-inline'", 'https://newassets.hcaptcha.com', ...CORS_TRUSTED_CDN_HOSTS], + 'img-src': ["'self'", 'data:', 'https://contrib.rocks', ...CORS_TRUSTED_CDN_HOSTS], + 'font-src': ["'self'", ...CORS_TRUSTED_CDN_HOSTS], + 'connect-src': ["'self'", 'https://hcaptcha.com', 'https://*.hcaptcha.com', 'https://challenges.cloudflare.com'] } }, frameguard: false, diff --git a/apps/web/middleware/helmet.js b/apps/web/middleware/helmet.js index 55ed090..20f407e 100644 --- a/apps/web/middleware/helmet.js +++ b/apps/web/middleware/helmet.js @@ -1,65 +1,24 @@ import helmet from 'helmet' -/** @returns {string[]} */ - -/** - * Splits a comma-separated string of hosts into an array of trimmed host strings. - * @param {string} hosts - A comma-separated string of hostnames (e.g., "example.com, api.example.com") - * @returns {string[]} An array of trimmed hostnames (e.g., ["example.com", "api.example.com"]) - */ -const splitHosts = (hosts) => { - if (!hosts) return [] - return hosts.split(',').map(h => h.trim()) -} - +const CORS_TRUSTED_HOSTS = process.env.CORS_TRUSTED_HOSTS ? process.env.CORS_TRUSTED_HOSTS.split(',').map(host => host.trim()) : [] +const CORS_TRUSTED_CDN_HOSTS = process.env.CORS_TRUSTED_CDN_HOSTS + ? process.env.CORS_TRUSTED_CDN_HOSTS.split(',').map(host => host.trim()) + : [] const isDevelopment = process.env.NODE_ENV !== 'production' -const defaultDirectives = helmet.contentSecurityPolicy.getDefaultDirectives() -if (isDevelopment) delete defaultDirectives['upgrade-insecure-requests'] - const helmetMiddleware = helmet({ contentSecurityPolicy: { directives: { - ...defaultDirectives, - 'frame-ancestors': ["'self'", ...splitHosts(process.env.CORS_TRUSTED_HOSTS || '')], - 'frame-src': [ - "'self'", - 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', - 'https://challenges.cloudflare.com', - ...splitHosts(process.env.CORS_TRUSTED_HOSTS || '') - ], - 'script-src': [ - "'self'", "'unsafe-inline'", - 'https://static.cloudflareinsights.com', - 'https://js.hcaptcha.com', 'https://newassets.hcaptcha.com', - 'https://challenges.cloudflare.com', - 'https://www.googletagmanager.com', - 'https://www.google-analytics.com', - ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '') - ], - 'style-src': [ - "'self'", "'unsafe-inline'", - 'https://newassets.hcaptcha.com', - ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '') - ], - 'img-src': [ - "'self'", 'data:', - 'https://contrib.rocks', - 'https://www.google-analytics.com', - 'https://www.googletagmanager.com', - ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '') - ], - 'font-src': ["'self'", ...splitHosts(process.env.CORS_TRUSTED_CDN_HOSTS || '')], - 'connect-src': [ - "'self'", - 'https://hcaptcha.com', 'https://*.hcaptcha.com', - 'https://challenges.cloudflare.com', - 'https://pat-issuer.cloudflare.com', - 'https://www.google-analytics.com', - 'https://analytics.google.com', - 'https://stats.g.doubleclick.net', - 'https://*.google-analytics.com' - ] + ...helmet.contentSecurityPolicy.getDefaultDirectives(), + + 'upgrade-insecure-requests': isDevelopment ? null : [], + 'frame-ancestors': ["'self'", ...CORS_TRUSTED_HOSTS], + 'frame-src': ["'self'", 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_HOSTS], + 'script-src': ["'self'", "'unsafe-inline'", 'https://static.cloudflareinsights.com', 'https://js.hcaptcha.com', 'https://newassets.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_CDN_HOSTS], + 'style-src': ["'self'", "'unsafe-inline'", 'https://newassets.hcaptcha.com', ...CORS_TRUSTED_CDN_HOSTS], + 'img-src': ["'self'", 'data:', 'https://contrib.rocks', ...CORS_TRUSTED_CDN_HOSTS], + 'font-src': ["'self'", ...CORS_TRUSTED_CDN_HOSTS], + 'connect-src': ["'self'", 'https://hcaptcha.com', 'https://*.hcaptcha.com', 'https://challenges.cloudflare.com'] } }, frameguard: false, From c1bccc366b397c48cbbaca8b4394f20440d85f2d Mon Sep 17 00:00:00 2001 From: GTPSHAX Date: Sun, 17 May 2026 13:48:28 +0700 Subject: [PATCH 10/10] fix: refactor CORS trusted hosts handling and enhance content security policy directives --- apps/api/middleware/helmet.js | 60 ++++++++++++++++++++++++++++------- apps/web/middleware/helmet.js | 60 ++++++++++++++++++++++++++++------- 2 files changed, 96 insertions(+), 24 deletions(-) diff --git a/apps/api/middleware/helmet.js b/apps/api/middleware/helmet.js index 20f407e..2effb00 100644 --- a/apps/api/middleware/helmet.js +++ b/apps/api/middleware/helmet.js @@ -1,24 +1,60 @@ import helmet from 'helmet' -const CORS_TRUSTED_HOSTS = process.env.CORS_TRUSTED_HOSTS ? process.env.CORS_TRUSTED_HOSTS.split(',').map(host => host.trim()) : [] -const CORS_TRUSTED_CDN_HOSTS = process.env.CORS_TRUSTED_CDN_HOSTS - ? process.env.CORS_TRUSTED_CDN_HOSTS.split(',').map(host => host.trim()) - : [] +/** @returns {string[]} */ +const parseEnvHosts = (/** @type {string} */ key) => + process.env[key]?.split(',').map(h => h.trim()) ?? [] + +const CORS_TRUSTED_HOSTS = parseEnvHosts('CORS_TRUSTED_HOSTS') +const CORS_TRUSTED_CDN_HOSTS = parseEnvHosts('CORS_TRUSTED_CDN_HOSTS') const isDevelopment = process.env.NODE_ENV !== 'production' +const defaultDirectives = helmet.contentSecurityPolicy.getDefaultDirectives() +if (isDevelopment) delete defaultDirectives['upgrade-insecure-requests'] + const helmetMiddleware = helmet({ contentSecurityPolicy: { directives: { - ...helmet.contentSecurityPolicy.getDefaultDirectives(), - - 'upgrade-insecure-requests': isDevelopment ? null : [], + ...defaultDirectives, 'frame-ancestors': ["'self'", ...CORS_TRUSTED_HOSTS], - 'frame-src': ["'self'", 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_HOSTS], - 'script-src': ["'self'", "'unsafe-inline'", 'https://static.cloudflareinsights.com', 'https://js.hcaptcha.com', 'https://newassets.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_CDN_HOSTS], - 'style-src': ["'self'", "'unsafe-inline'", 'https://newassets.hcaptcha.com', ...CORS_TRUSTED_CDN_HOSTS], - 'img-src': ["'self'", 'data:', 'https://contrib.rocks', ...CORS_TRUSTED_CDN_HOSTS], + 'frame-src': [ + "'self'", + 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', + 'https://challenges.cloudflare.com', + ...CORS_TRUSTED_HOSTS + ], + 'script-src': [ + "'self'", "'unsafe-inline'", + 'https://static.cloudflareinsights.com', + 'https://js.hcaptcha.com', 'https://newassets.hcaptcha.com', + 'https://challenges.cloudflare.com', + 'https://www.googletagmanager.com', + 'https://www.google-analytics.com', + ...CORS_TRUSTED_CDN_HOSTS + ], + 'style-src': [ + "'self'", "'unsafe-inline'", + 'https://newassets.hcaptcha.com', + ...CORS_TRUSTED_CDN_HOSTS + ], + 'img-src': [ + "'self'", 'data:', + 'https://contrib.rocks', + 'https://www.google-analytics.com', + 'https://www.googletagmanager.com', + ...CORS_TRUSTED_CDN_HOSTS + ], 'font-src': ["'self'", ...CORS_TRUSTED_CDN_HOSTS], - 'connect-src': ["'self'", 'https://hcaptcha.com', 'https://*.hcaptcha.com', 'https://challenges.cloudflare.com'] + 'connect-src': [ + "'self'", + 'https://hcaptcha.com', 'https://*.hcaptcha.com', + 'https://challenges.cloudflare.com', + 'https://pat-issuer.cloudflare.com', + 'https://www.google-analytics.com', + 'https://analytics.google.com', + 'https://stats.g.doubleclick.net', + 'https://*.google-analytics.com', + ...CORS_TRUSTED_HOSTS + ] } }, frameguard: false, diff --git a/apps/web/middleware/helmet.js b/apps/web/middleware/helmet.js index 20f407e..2effb00 100644 --- a/apps/web/middleware/helmet.js +++ b/apps/web/middleware/helmet.js @@ -1,24 +1,60 @@ import helmet from 'helmet' -const CORS_TRUSTED_HOSTS = process.env.CORS_TRUSTED_HOSTS ? process.env.CORS_TRUSTED_HOSTS.split(',').map(host => host.trim()) : [] -const CORS_TRUSTED_CDN_HOSTS = process.env.CORS_TRUSTED_CDN_HOSTS - ? process.env.CORS_TRUSTED_CDN_HOSTS.split(',').map(host => host.trim()) - : [] +/** @returns {string[]} */ +const parseEnvHosts = (/** @type {string} */ key) => + process.env[key]?.split(',').map(h => h.trim()) ?? [] + +const CORS_TRUSTED_HOSTS = parseEnvHosts('CORS_TRUSTED_HOSTS') +const CORS_TRUSTED_CDN_HOSTS = parseEnvHosts('CORS_TRUSTED_CDN_HOSTS') const isDevelopment = process.env.NODE_ENV !== 'production' +const defaultDirectives = helmet.contentSecurityPolicy.getDefaultDirectives() +if (isDevelopment) delete defaultDirectives['upgrade-insecure-requests'] + const helmetMiddleware = helmet({ contentSecurityPolicy: { directives: { - ...helmet.contentSecurityPolicy.getDefaultDirectives(), - - 'upgrade-insecure-requests': isDevelopment ? null : [], + ...defaultDirectives, 'frame-ancestors': ["'self'", ...CORS_TRUSTED_HOSTS], - 'frame-src': ["'self'", 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_HOSTS], - 'script-src': ["'self'", "'unsafe-inline'", 'https://static.cloudflareinsights.com', 'https://js.hcaptcha.com', 'https://newassets.hcaptcha.com', 'https://challenges.cloudflare.com', ...CORS_TRUSTED_CDN_HOSTS], - 'style-src': ["'self'", "'unsafe-inline'", 'https://newassets.hcaptcha.com', ...CORS_TRUSTED_CDN_HOSTS], - 'img-src': ["'self'", 'data:', 'https://contrib.rocks', ...CORS_TRUSTED_CDN_HOSTS], + 'frame-src': [ + "'self'", + 'https://newassets.hcaptcha.com', 'https://js.hcaptcha.com', + 'https://challenges.cloudflare.com', + ...CORS_TRUSTED_HOSTS + ], + 'script-src': [ + "'self'", "'unsafe-inline'", + 'https://static.cloudflareinsights.com', + 'https://js.hcaptcha.com', 'https://newassets.hcaptcha.com', + 'https://challenges.cloudflare.com', + 'https://www.googletagmanager.com', + 'https://www.google-analytics.com', + ...CORS_TRUSTED_CDN_HOSTS + ], + 'style-src': [ + "'self'", "'unsafe-inline'", + 'https://newassets.hcaptcha.com', + ...CORS_TRUSTED_CDN_HOSTS + ], + 'img-src': [ + "'self'", 'data:', + 'https://contrib.rocks', + 'https://www.google-analytics.com', + 'https://www.googletagmanager.com', + ...CORS_TRUSTED_CDN_HOSTS + ], 'font-src': ["'self'", ...CORS_TRUSTED_CDN_HOSTS], - 'connect-src': ["'self'", 'https://hcaptcha.com', 'https://*.hcaptcha.com', 'https://challenges.cloudflare.com'] + 'connect-src': [ + "'self'", + 'https://hcaptcha.com', 'https://*.hcaptcha.com', + 'https://challenges.cloudflare.com', + 'https://pat-issuer.cloudflare.com', + 'https://www.google-analytics.com', + 'https://analytics.google.com', + 'https://stats.g.doubleclick.net', + 'https://*.google-analytics.com', + ...CORS_TRUSTED_HOSTS + ] } }, frameguard: false,