diff --git a/.astro/astro/content.d.ts b/.astro/astro/content.d.ts index 8893335..0abe462 100644 --- a/.astro/astro/content.d.ts +++ b/.astro/astro/content.d.ts @@ -4,7 +4,6 @@ declare module 'astro:content' { Content: import('astro').MarkdownInstance<{}>['Content']; headings: import('astro').MarkdownHeading[]; remarkPluginFrontmatter: Record; - components: import('astro').MDXInstance<{}>['components']; }>; } } diff --git a/.astro/settings.json b/.astro/settings.json index 9bb4082..0681755 100644 --- a/.astro/settings.json +++ b/.astro/settings.json @@ -1,5 +1,5 @@ { "_variables": { - "lastUpdateCheck": 1772971528213 + "lastUpdateCheck": 1775927117614 } } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2eee3a1..ba79cfb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,68 +1,43 @@ # syntax=docker/dockerfile:1 ARG NODE_VERSION=20-alpine -ARG NGINX_VERSION=alpine FROM node:${NODE_VERSION} AS builder -# Install build dependencies -RUN apk add --no-cache libc6-compat +# Public envs used by Astro in client-side code must exist at build time. +ARG PUBLIC_RECAPTCHA_SITE_KEY +ENV PUBLIC_RECAPTCHA_SITE_KEY=${PUBLIC_RECAPTCHA_SITE_KEY} -# Configure pnpm ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable WORKDIR /app -# Copy dependency files first for better caching COPY package.json pnpm-lock.yaml ./ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile -# Copy source code COPY . . +RUN pnpm run build && find /app/dist -name "*.map" -delete && pnpm prune --prod -# Build application with optimizations -RUN pnpm run build && \ - # Remove source maps and unnecessary files - find /app/dist -name "*.map" -delete && \ - # Remove dev dependencies - pnpm prune --prod +FROM node:${NODE_VERSION} AS runtime -# Production stage -FROM nginx:${NGINX_VERSION} AS runtime +ENV NODE_ENV=production +ENV HOST=0.0.0.0 +ENV PORT=4321 -# Install security updates and curl for health check -RUN apk --no-cache upgrade && \ - apk --no-cache add curl - -# Fix permissions for nginx user -RUN chown -R nginx:nginx /var/cache/nginx && \ - chown -R nginx:nginx /var/log/nginx && \ - chown -R nginx:nginx /etc/nginx/conf.d && \ - touch /var/run/nginx.pid && \ - chown -R nginx:nginx /var/run/nginx.pid - -# Remove default nginx assets -RUN rm -rf /usr/share/nginx/html/* - -# Copy built application with proper ownership -COPY --from=builder --chown=nginx:nginx /app/dist /usr/share/nginx/html +WORKDIR /app -# Remove default nginx config -RUN rm /etc/nginx/conf.d/default.conf +RUN addgroup -S app && adduser -S -G app app -# Copy custom nginx config with proper ownership -COPY --chown=nginx:nginx nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder --chown=app:app /app/dist ./dist +COPY --from=builder --chown=app:app /app/package.json ./package.json +COPY --from=builder --chown=app:app /app/node_modules ./node_modules -# Switch to non-root user -USER nginx +USER app -# Expose port -EXPOSE 8080 +EXPOSE 4321 -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:8080/ || exit 1 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:4321/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" -# Start nginx -ENTRYPOINT ["nginx", "-g", "daemon off;"] +CMD ["node", "./dist/server/entry.mjs"] diff --git a/docker-compose.yml b/docker-compose.yml index 3ba4d38..f880dfb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,22 @@ services: hackiit-website: - build: . + build: + context: . + args: + PUBLIC_RECAPTCHA_SITE_KEY: ${PUBLIC_RECAPTCHA_SITE_KEY} container_name: hackiit-website + environment: + - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} + - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID} + - RECAPTCHA_SECRET_KEY=${RECAPTCHA_SECRET_KEY} + - HOST=0.0.0.0 + - PORT=4321 ports: - - "8080:8080" + - "8080:4321" restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/"] + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4321/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 30s - timeout: 3s + timeout: 5s retries: 3 - start_period: 5s + start_period: 10s diff --git a/package-lock.json b/package-lock.json index 46c5929..bd128a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "@onwidget/astrowind", "version": "1.0.0-beta.48", "dependencies": { + "@astrojs/node": "^8.3.4", "@astrojs/rss": "^4.0.8", "@astrojs/sitemap": "^3.2.0", "@astrolib/analytics": "^0.6.1", @@ -321,6 +322,19 @@ "astro": "^4.8.0" } }, + "node_modules/@astrojs/node": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@astrojs/node/-/node-8.3.4.tgz", + "integrity": "sha512-xzQs39goN7xh9np9rypGmbgZj3AmmjNxEMj9ZWz5aBERlqqFF3n8A/w/uaJeZ/bkHS60l1BXVS0tgsQt9MFqBA==", + "license": "MIT", + "dependencies": { + "send": "^0.19.0", + "server-destroy": "^1.0.1" + }, + "peerDependencies": { + "astro": "^4.2.0" + } + }, "node_modules/@astrojs/partytown": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@astrojs/partytown/-/partytown-2.1.2.tgz", @@ -455,6 +469,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -803,6 +818,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1507,6 +1523,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1529,6 +1546,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1551,6 +1569,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1567,6 +1586,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1583,6 +1603,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1599,6 +1620,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1615,6 +1637,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1631,6 +1654,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1647,6 +1671,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1663,6 +1688,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1679,6 +1705,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1701,6 +1728,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1723,6 +1751,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1745,6 +1774,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1767,6 +1797,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1789,6 +1820,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1811,6 +1843,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -1830,6 +1863,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1849,6 +1883,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2736,6 +2771,7 @@ "integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.18.1", "@typescript-eslint/types": "8.18.1", @@ -3002,6 +3038,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3200,6 +3237,7 @@ "resolved": "https://registry.npmjs.org/astro/-/astro-4.16.19.tgz", "integrity": "sha512-baeSswPC5ZYvhGDoj25L2FuzKRWMgx105FetOPQVJFMCAp0o08OonYC7AhwsFdhvp7GapqjnC1Fe3lKb2lupYw==", "license": "MIT", + "peer": true, "dependencies": { "@astrojs/compiler": "^2.10.3", "@astrojs/internal-helpers": "0.4.1", @@ -3569,6 +3607,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -4228,6 +4267,15 @@ "node": ">=16.0.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4237,6 +4285,16 @@ "node": ">=6" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -4398,6 +4456,12 @@ "dev": true, "license": "MIT" }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.74", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz", @@ -4433,6 +4497,15 @@ "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", @@ -4554,6 +4627,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4573,6 +4652,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4970,6 +5050,15 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -5269,6 +5358,15 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5833,6 +5931,26 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "license": "BSD-2-Clause" }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -5892,6 +6010,12 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/inline-style-parser": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", @@ -6272,6 +6396,7 @@ "integrity": "sha512-8f7aNmS1+etYSLHht0fQApPc2kNO8qGRutifN5rVIc6Xo6ABsEbqOr758UwI7ALVbTt4x1fllKt0PYgzD9S3yQ==", "devOptional": true, "license": "MPL-2.0", + "peer": true, "dependencies": { "detect-libc": "^1.0.3" }, @@ -6302,6 +6427,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6322,6 +6448,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6342,6 +6469,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6362,6 +6490,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6382,6 +6511,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6402,6 +6532,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6422,6 +6553,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6442,6 +6574,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6462,6 +6595,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6482,6 +6616,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7864,6 +7999,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -8092,6 +8239,18 @@ "node": ">= 6" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -8625,6 +8784,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", @@ -8799,6 +8959,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -8815,6 +8976,7 @@ "integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@astrojs/compiler": "^2.9.1", "prettier": "^3.0.0", @@ -8921,6 +9083,15 @@ ], "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -9377,6 +9548,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.6" }, @@ -9487,6 +9659,57 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/sharp": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", @@ -9702,6 +9925,15 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -10039,6 +10271,7 @@ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -10180,6 +10413,7 @@ "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", "devOptional": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -10258,6 +10492,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -10322,7 +10565,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "devOptional": true, + "dev": true, "license": "0BSD" }, "node_modules/type-check": { @@ -10362,6 +10605,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10708,6 +10952,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -11438,6 +11683,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/components/common/SocialShare.astro b/src/components/common/SocialShare.astro index fe83e0d..de4d6aa 100644 --- a/src/components/common/SocialShare.astro +++ b/src/components/common/SocialShare.astro @@ -53,12 +53,12 @@ const { text, url, class: className = 'inline-block' } = Astro.props; diff --git a/src/components/ui/Form.astro b/src/components/ui/Form.astro index 95f92e7..9ccad96 100644 --- a/src/components/ui/Form.astro +++ b/src/components/ui/Form.astro @@ -226,7 +226,9 @@ const siteKey = import.meta.env.PUBLIC_RECAPTCHA_SITE_KEY; method: 'POST', body: formData }); - const data = await fetchRes.json(); + const contentType = fetchRes.headers.get('content-type') || ''; + const isJson = contentType.includes('application/json'); + const data = isJson ? await fetchRes.json() : null; if (messageDiv) { messageDiv.classList.remove('hidden'); @@ -239,7 +241,7 @@ const siteKey = import.meta.env.PUBLIC_RECAPTCHA_SITE_KEY; } else { // Error de API messageDiv.className = 'mt-4 p-4 rounded-lg bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200'; - messageDiv.textContent = data.error || 'Hubo un problema al enviar el mensaje.'; + messageDiv.textContent = data?.error || `No se pudo enviar el formulario (HTTP ${fetchRes.status}).`; window.grecaptcha.reset(window.currentWidgetId); } } diff --git a/src/data/contactFormConfig.ts b/src/data/contactFormConfig.ts new file mode 100644 index 0000000..b8543b3 --- /dev/null +++ b/src/data/contactFormConfig.ts @@ -0,0 +1,21 @@ +export const contactFormConfig = { + inputs: [ + { + type: 'text', + name: 'name', + label: 'Nombre', + }, + { + type: 'text', + name: 'telegram', + label: 'Usuario de telegram', + }, + ], + textarea: { + label: 'Mensaje', + }, + disclaimer: { + label: 'Al enviar este formulario de contacto, reconoces y aceptas la recopilación de tu información personal.', + }, + description: 'Nuestro equipo de soporte generalmente responde dentro de las 24 horas hábiles.', +}; diff --git a/src/navigation.ts b/src/navigation.ts index 7ae9b65..350021f 100644 --- a/src/navigation.ts +++ b/src/navigation.ts @@ -28,6 +28,10 @@ export const headerData = { { text: 'Contacto', href: getPermalink('/contact'), + }, + { + text: 'Únete al club', + href: getPermalink('/unete'), } ], } diff --git a/src/pages/api/contact.ts b/src/pages/api/contact.ts index 0697c40..d708fb6 100644 --- a/src/pages/api/contact.ts +++ b/src/pages/api/contact.ts @@ -6,9 +6,9 @@ export const prerender = false; // Esquema de sanitización con Zod const ContactSchema = z.object({ name: z.string().min(2).max(100).trim(), - email: z.string().email().toLowerCase().trim(), + telegram: z.string().min(2).max(100).trim(), message: z.string().min(10).max(2000).trim(), - 'g-recaptcha-response': z.string().min(1, "Captcha obligatorio"), + 'g-recaptcha-response': z.string().min(1, 'Captcha obligatorio'), }); // Función de escape para evitar inyecciones en el bot de Telegram @@ -35,11 +35,16 @@ function checkRateLimit(ip: string): boolean { export const POST: APIRoute = async ({ request, clientAddress }) => { const ip = clientAddress || 'unknown'; + const jsonHeaders = { 'Content-Type': 'application/json; charset=utf-8' }; + const genericError = { error: 'No se pudo procesar la solicitud.' }; try { // Rate Limiting if (checkRateLimit(ip)) { - return new Response(JSON.stringify({ error: 'Límite excedido. Reintenta en 5 min.' }), { status: 429 }); + return new Response(JSON.stringify(genericError), { + status: 429, + headers: jsonHeaders, + }); } // Validación de datos de entrada @@ -48,14 +53,23 @@ export const POST: APIRoute = async ({ request, clientAddress }) => { const validatedData = ContactSchema.safeParse(payload); if (!validatedData.success) { - return new Response(JSON.stringify({ error: 'Datos no válidos' }), { status: 400 }); + return new Response(JSON.stringify(genericError), { status: 400, headers: jsonHeaders }); } - const { name, email, message, 'g-recaptcha-response': captchaToken } = validatedData.data; + const { name, telegram, message, 'g-recaptcha-response': captchaToken } = validatedData.data; // Verificación con la API de Google const googleVerifyUrl = 'https://www.google.com/recaptcha/api/siteverify'; - const secretKey = import.meta.env.RECAPTCHA_SECRET_KEY; + const secretKey = process.env.RECAPTCHA_SECRET_KEY; + const botToken = process.env.TELEGRAM_BOT_TOKEN; + const chatId = process.env.TELEGRAM_CHAT_ID; + + if (!secretKey || !botToken || !chatId) { + return new Response(JSON.stringify(genericError), { + status: 500, + headers: jsonHeaders, + }); + } const captchaRes = await fetch(googleVerifyUrl, { method: 'POST', @@ -67,20 +81,27 @@ export const POST: APIRoute = async ({ request, clientAddress }) => { }), }); + if (!captchaRes.ok) { + return new Response(JSON.stringify(genericError), { + status: 502, + headers: jsonHeaders, + }); + } + const captchaResult = await captchaRes.json(); if (!captchaResult.success) { - return new Response(JSON.stringify({ error: 'Fallo en la validación del Captcha' }), { status: 403 }); + return new Response(JSON.stringify(genericError), { + status: 403, + headers: jsonHeaders, + }); } // Envío a Telegram con escape de caracteres - const botToken = import.meta.env.TELEGRAM_BOT_TOKEN; - const chatId = import.meta.env.TELEGRAM_CHAT_ID; - const telegramMessage = ` Mensaje desde la Web Nombre: ${escapeHTML(name)} -Email: ${escapeHTML(email)} +Telegram: ${escapeHTML(telegram)} Mensaje: ${escapeHTML(message)} `; @@ -95,12 +116,17 @@ ${escapeHTML(message)} }), }); - if (!telegramRes.ok) throw new Error('Error en API de Telegram'); + if (!telegramRes.ok) { + return new Response(JSON.stringify(genericError), { + status: 502, + headers: jsonHeaders, + }); + } - return new Response(JSON.stringify({ success: true }), { status: 200 }); + return new Response(JSON.stringify({ success: true }), { status: 200, headers: jsonHeaders }); } catch (error) { console.error('Server Error:', error); - return new Response(JSON.stringify({ error: 'Error interno' }), { status: 500 }); + return new Response(JSON.stringify(genericError), { status: 500, headers: jsonHeaders }); } }; \ No newline at end of file diff --git a/src/pages/contact.astro b/src/pages/contact.astro index 30ac51e..723c286 100644 --- a/src/pages/contact.astro +++ b/src/pages/contact.astro @@ -3,6 +3,7 @@ import Layout from '~/layouts/PageLayout.astro'; import HeroText from '~/components/widgets/HeroText.astro'; import ContactUs from '~/components/widgets/Contact.astro'; import Features2 from '~/components/widgets/Features2.astro'; +import { contactFormConfig } from '~/data/contactFormConfig'; const metadata = { title: 'Contacto', @@ -18,26 +19,7 @@ const metadata = { id="form" title="¿Tienes alguna pregunta?" subtitle="Ponte en contacto con nosotros a través del siguiente formulario y te responderemos lo más rápido posible." - inputs={[ - { - type: 'text', - name: 'name', - label: 'Nombre', - }, - { - type: 'email', - name: 'email', - label: 'Correo electrónico', - }, - ]} - textarea={{ - label: 'Mensaje', - }} - disclaimer={{ - label: - 'Al enviar este formulario de contacto, reconoces y aceptas la recopilación de tu información personal.', - }} - description="Nuestro equipo de soporte generalmente responde dentro de las 24 horas hábiles." + {...contactFormConfig} /> + + + +