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 1f2c4a2..2effb00 100644 --- a/apps/web/middleware/helmet.js +++ b/apps/web/middleware/helmet.js @@ -1,41 +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()) - : [] -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], + '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', ...(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', + '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/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' }) - } -}) diff --git a/packages/handlers/handle-generate-docx.js b/packages/handlers/handle-generate-docx.js index 6aed508..de00f94 100644 --- a/packages/handlers/handle-generate-docx.js +++ b/packages/handlers/handle-generate-docx.js @@ -129,12 +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 (!await handleHCaptchaValidation(body)) { - return { status: 400, message: 'hCaptcha validation failed' } - } - if (!await handleCloudflareCaptchaValidation(body, remoteip)) { - return { status: 400, message: 'Cloudflare Captcha 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'