diff --git a/.dockerignore b/.dockerignore index 80455a8..0174ae4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,9 @@ dist .env .DS_Store *.log +backend +frontend/node_modules +frontend/dist +frontend/coverage +frontend/test-results +frontend/playwright-report diff --git a/backend/.dockerignore b/backend/.dockerignore index 567bf5c..e526d44 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -9,3 +9,7 @@ dist *.log prisma/dev.db prisma/dev.db-journal +src/generated +coverage +*.test.ts +*.spec.ts diff --git a/backend/Dockerfile b/backend/Dockerfile index 53589ec..e50b0ee 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -3,12 +3,15 @@ FROM node:20-alpine AS builder WORKDIR /app +# Native build deps for modules that may compile from source (e.g., better-sqlite3 on arm64) +RUN apk add --no-cache python3 make g++ + # Copy package files COPY package*.json ./ COPY tsconfig.json ./ # Install dependencies -RUN npm ci +RUN npm ci && npm cache clean --force # Copy prisma schema COPY prisma ./prisma/ @@ -25,7 +28,7 @@ RUN npx tsc # Production stage FROM node:20-alpine -# Install OpenSSL for Prisma and su-exec, create non-root user +# Install runtime packages and create non-root user RUN apk add --no-cache openssl su-exec && \ addgroup -g 1001 -S nodejs && \ adduser -S nodejs -u 1001 @@ -36,7 +39,10 @@ WORKDIR /app COPY package*.json ./ # Install production dependencies only -RUN npm ci --only=production +RUN apk add --no-cache --virtual .build-deps python3 make g++ && \ + npm ci --omit=dev && \ + npm cache clean --force && \ + apk del .build-deps # Copy prisma schema and migrations for runtime and hydration template COPY prisma ./prisma/ @@ -48,9 +54,6 @@ COPY --from=builder /app/dist ./dist # Copy the generated Prisma Client from builder to maintain the same structure COPY --from=builder /app/src/generated ./dist/generated -# Generate Prisma Client in production (updates node_modules) -RUN npx prisma generate - # Create necessary directories (ownership will be set in entrypoint) RUN mkdir -p /app/uploads /app/prisma diff --git a/backend/package-lock.json b/backend/package-lock.json index 66160ea..0cf4439 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,23 +1,15 @@ { "name": "backend", - "version": "0.3.2", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "backend", - "version": "0.3.2", + "version": "0.4.0", "license": "ISC", "dependencies": { "@prisma/client": "^5.22.0", - "@types/archiver": "^7.0.0", - "@types/bcrypt": "^6.0.0", - "@types/jsdom": "^21.1.7", - "@types/jsonwebtoken": "^9.0.10", - "@types/ms": "^2.1.0", - "@types/multer": "^2.0.0", - "@types/socket.io": "^3.0.1", - "@types/uuid": "^10.0.0", "archiver": "^7.0.1", "bcrypt": "^6.0.0", "better-sqlite3": "^12.4.6", @@ -38,10 +30,18 @@ "zod": "^4.1.12" }, "devDependencies": { + "@types/archiver": "^7.0.0", + "@types/bcrypt": "^6.0.0", "@types/cors": "^2.8.19", "@types/express": "^5.0.5", + "@types/jsdom": "^21.1.7", + "@types/jsonwebtoken": "^9.0.10", + "@types/ms": "^2.1.0", + "@types/multer": "^2.0.0", "@types/node": "^24.10.1", + "@types/socket.io": "^3.0.1", "@types/supertest": "^6.0.3", + "@types/uuid": "^10.0.0", "nodemon": "^3.1.11", "supertest": "^7.1.4", "ts-node": "^10.9.2", @@ -1007,6 +1007,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", + "dev": true, "license": "MIT", "dependencies": { "@types/readdir-glob": "*" @@ -1016,6 +1017,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -1025,6 +1027,7 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -1046,6 +1049,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -1085,6 +1089,7 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -1096,6 +1101,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -1108,12 +1114,14 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, "license": "MIT" }, "node_modules/@types/jsdom": { "version": "21.1.7", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -1125,6 +1133,7 @@ "version": "9.0.10", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, "license": "MIT", "dependencies": { "@types/ms": "*", @@ -1142,18 +1151,21 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, "license": "MIT" }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, "license": "MIT" }, "node_modules/@types/multer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, "license": "MIT", "dependencies": { "@types/express": "*" @@ -1164,7 +1176,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1173,18 +1184,21 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/readdir-glob": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -1194,6 +1208,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -1203,6 +1218,7 @@ "version": "1.15.10", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -1214,6 +1230,7 @@ "version": "0.17.6", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -1224,6 +1241,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", + "dev": true, "license": "MIT", "dependencies": { "socket.io": "*" @@ -1257,6 +1275,7 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, "license": "MIT" }, "node_modules/@types/trusted-types": { @@ -1270,6 +1289,7 @@ "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, "license": "MIT" }, "node_modules/@vitest/expect": { @@ -2655,7 +2675,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.0.tgz", "integrity": "sha512-XdpJDLxfztVY59X0zPI6sibRiGcxhTPXRD3IhJmjKf2jwMvkRGV1j7loB8U+heeamoU3XvihAaGRTR4aXXUN3A==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -4090,7 +4109,6 @@ "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/engines": "5.22.0" }, @@ -5103,7 +5121,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5262,7 +5279,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5353,7 +5369,6 @@ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -5447,7 +5462,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/backend/package.json b/backend/package.json index 3dae9a0..184bf9a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,14 +17,6 @@ "type": "commonjs", "dependencies": { "@prisma/client": "^5.22.0", - "@types/archiver": "^7.0.0", - "@types/bcrypt": "^6.0.0", - "@types/jsdom": "^21.1.7", - "@types/jsonwebtoken": "^9.0.10", - "@types/ms": "^2.1.0", - "@types/multer": "^2.0.0", - "@types/socket.io": "^3.0.1", - "@types/uuid": "^10.0.0", "archiver": "^7.0.1", "bcrypt": "^6.0.0", "better-sqlite3": "^12.4.6", @@ -45,10 +37,18 @@ "zod": "^4.1.12" }, "devDependencies": { + "@types/archiver": "^7.0.0", + "@types/bcrypt": "^6.0.0", "@types/cors": "^2.8.19", "@types/express": "^5.0.5", + "@types/jsdom": "^21.1.7", + "@types/jsonwebtoken": "^9.0.10", + "@types/ms": "^2.1.0", + "@types/multer": "^2.0.0", "@types/node": "^24.10.1", + "@types/socket.io": "^3.0.1", "@types/supertest": "^6.0.3", + "@types/uuid": "^10.0.0", "nodemon": "^3.1.11", "supertest": "^7.1.4", "ts-node": "^10.9.2", diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 93821c2..9f3f266 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -7,7 +7,7 @@ WORKDIR /app/frontend COPY frontend/package*.json ./ # Install dependencies -RUN npm ci +RUN npm ci && npm cache clean --force # Copy source code and config files COPY frontend/ ./ @@ -25,9 +25,6 @@ RUN npm run build # Production stage FROM nginx:alpine -# Install envsubst (gettext) so we can template nginx config at runtime -RUN apk add --no-cache gettext - # Copy nginx config template (will be processed at runtime) COPY frontend/nginx.conf.template /etc/nginx/nginx.conf.template diff --git a/frontend/docker-entrypoint.sh b/frontend/docker-entrypoint.sh index f1ef869..a06b482 100644 --- a/frontend/docker-entrypoint.sh +++ b/frontend/docker-entrypoint.sh @@ -7,9 +7,9 @@ export BACKEND_URL="${BACKEND_URL:-backend:8000}" echo "Configuring nginx with BACKEND_URL: ${BACKEND_URL}" -# Substitute environment variables in nginx config template -# Only substitute BACKEND_URL, preserve nginx variables like $http_upgrade -envsubst '${BACKEND_URL}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf +# Replace only our custom placeholder and preserve nginx runtime vars like $http_upgrade +ESCAPED_BACKEND_URL=$(printf '%s\n' "$BACKEND_URL" | sed 's/[\/&]/\\&/g') +sed "s/__BACKEND_URL__/${ESCAPED_BACKEND_URL}/g" /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf # Validate the generated nginx configuration before starting echo "Validating nginx configuration..." diff --git a/frontend/nginx.conf.template b/frontend/nginx.conf.template index 91df63c..df759ac 100644 --- a/frontend/nginx.conf.template +++ b/frontend/nginx.conf.template @@ -24,7 +24,7 @@ http { # API and WebSocket proxy to backend # BACKEND_URL is substituted at container startup (default: backend:8000) location /api/ { - proxy_pass http://${BACKEND_URL}/; + proxy_pass http://__BACKEND_URL__/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -49,7 +49,7 @@ http { # WebSocket proxy for Socket.IO location /socket.io/ { - proxy_pass http://${BACKEND_URL}/socket.io/; + proxy_pass http://__BACKEND_URL__/socket.io/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade';