Compare commits

..

3 Commits

Author SHA1 Message Date
Zimeng Xiong 812f1cbf58 chore: pre-release v0.4.1-dev 2026-02-07 10:01:14 -08:00
Zimeng Xiong 26017fa5d2 fix JWT secret 2026-02-07 10:00:58 -08:00
Zimeng Xiong 06f4c0f537 remove dev dependencies from development containers 2026-02-07 09:27:39 -08:00
14 changed files with 102 additions and 46 deletions
+6
View File
@@ -7,3 +7,9 @@ dist
.env .env
.DS_Store .DS_Store
*.log *.log
backend
frontend/node_modules
frontend/dist
frontend/coverage
frontend/test-results
frontend/playwright-report
+4 -1
View File
@@ -99,6 +99,8 @@ docker compose -f docker-compose.prod.yml up -d
# Access the frontend at localhost:6767 # Access the frontend at localhost:6767
``` ```
For single-container deployments, `JWT_SECRET` can be omitted and will be auto-generated and persisted in the backend volume on first start. For portability and all multi-instance deployments, set a fixed `JWT_SECRET` explicitly.
## Docker Build ## Docker Build
[Install Docker](https://docs.docker.com/desktop/) [Install Docker](https://docs.docker.com/desktop/)
@@ -141,7 +143,7 @@ frontend:
### Multi-Container / Kubernetes Deployments ### Multi-Container / Kubernetes Deployments
When running multiple backend replicas (e.g., Kubernetes, Docker Swarm, or load-balanced containers), you **must** set the `CSRF_SECRET` environment variable to the same value across all instances. When running multiple backend replicas (e.g., Kubernetes, Docker Swarm, or load-balanced containers), you **must** set both `JWT_SECRET` and `CSRF_SECRET` to the same values across all instances.
```bash ```bash
# Generate a secure secret # Generate a secure secret
@@ -152,6 +154,7 @@ openssl rand -base64 32
# docker-compose.yml or k8s deployment # docker-compose.yml or k8s deployment
backend: backend:
environment: environment:
- JWT_SECRET=your-generated-jwt-secret-here
- CSRF_SECRET=your-generated-secret-here - CSRF_SECRET=your-generated-secret-here
``` ```
+1 -1
View File
@@ -1 +1 @@
0.4.0 0.4.1
+4
View File
@@ -9,3 +9,7 @@ dist
*.log *.log
prisma/dev.db prisma/dev.db
prisma/dev.db-journal prisma/dev.db-journal
src/generated
coverage
*.test.ts
*.spec.ts
+9 -6
View File
@@ -3,12 +3,15 @@ FROM node:20-alpine AS builder
WORKDIR /app 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 files
COPY package*.json ./ COPY package*.json ./
COPY tsconfig.json ./ COPY tsconfig.json ./
# Install dependencies # Install dependencies
RUN npm ci RUN npm ci && npm cache clean --force
# Copy prisma schema # Copy prisma schema
COPY prisma ./prisma/ COPY prisma ./prisma/
@@ -25,7 +28,7 @@ RUN npx tsc
# Production stage # Production stage
FROM node:20-alpine 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 && \ RUN apk add --no-cache openssl su-exec && \
addgroup -g 1001 -S nodejs && \ addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 adduser -S nodejs -u 1001
@@ -36,7 +39,10 @@ WORKDIR /app
COPY package*.json ./ COPY package*.json ./
# Install production dependencies only # 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 schema and migrations for runtime and hydration template
COPY prisma ./prisma/ 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 the generated Prisma Client from builder to maintain the same structure
COPY --from=builder /app/src/generated ./dist/generated 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) # Create necessary directories (ownership will be set in entrypoint)
RUN mkdir -p /app/uploads /app/prisma RUN mkdir -p /app/uploads /app/prisma
+25
View File
@@ -1,6 +1,30 @@
#!/bin/sh #!/bin/sh
set -e set -e
JWT_SECRET_FILE="/app/prisma/.jwt_secret"
# Ensure JWT secret exists for production startup.
# Backward compatibility: older installs may not have JWT_SECRET configured.
if [ -z "${JWT_SECRET:-}" ]; then
echo "JWT_SECRET not provided, resolving persisted secret..."
if [ -f "${JWT_SECRET_FILE}" ]; then
JWT_SECRET="$(tr -d '\r\n' < "${JWT_SECRET_FILE}")"
fi
if [ -z "${JWT_SECRET}" ]; then
echo "No persisted JWT secret found. Generating a new secret..."
JWT_SECRET="$(openssl rand -hex 32)"
umask 077
printf "%s" "${JWT_SECRET}" > "${JWT_SECRET_FILE}"
fi
else
# Persist explicitly provided secret to support future restarts without env injection.
umask 077
printf "%s" "${JWT_SECRET}" > "${JWT_SECRET_FILE}"
fi
export JWT_SECRET
# 1. Hydrate volume if empty (Running as root) # 1. Hydrate volume if empty (Running as root)
if [ ! -f "/app/prisma/schema.prisma" ]; then if [ ! -f "/app/prisma/schema.prisma" ]; then
echo "Mount is empty. Hydrating /app/prisma..." echo "Mount is empty. Hydrating /app/prisma..."
@@ -18,6 +42,7 @@ echo "Fixing filesystem permissions..."
chown -R nodejs:nodejs /app/uploads chown -R nodejs:nodejs /app/uploads
chown -R nodejs:nodejs /app/prisma chown -R nodejs:nodejs /app/prisma
chmod 755 /app/uploads chmod 755 /app/uploads
chmod 600 "${JWT_SECRET_FILE}"
# Ensure database file has proper permissions # Ensure database file has proper permissions
if [ -f "/app/prisma/dev.db" ]; then if [ -f "/app/prisma/dev.db" ]; then
+31 -17
View File
@@ -1,23 +1,15 @@
{ {
"name": "backend", "name": "backend",
"version": "0.3.2", "version": "0.4.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "backend", "name": "backend",
"version": "0.3.2", "version": "0.4.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@prisma/client": "^5.22.0", "@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", "archiver": "^7.0.1",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"better-sqlite3": "^12.4.6", "better-sqlite3": "^12.4.6",
@@ -38,10 +30,18 @@
"zod": "^4.1.12" "zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
"@types/archiver": "^7.0.0",
"@types/bcrypt": "^6.0.0",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.5", "@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/node": "^24.10.1",
"@types/socket.io": "^3.0.1",
"@types/supertest": "^6.0.3", "@types/supertest": "^6.0.3",
"@types/uuid": "^10.0.0",
"nodemon": "^3.1.11", "nodemon": "^3.1.11",
"supertest": "^7.1.4", "supertest": "^7.1.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
@@ -1007,6 +1007,7 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz",
"integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/readdir-glob": "*" "@types/readdir-glob": "*"
@@ -1016,6 +1017,7 @@
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz",
"integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
@@ -1025,6 +1027,7 @@
"version": "1.19.6", "version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/connect": "*", "@types/connect": "*",
@@ -1046,6 +1049,7 @@
"version": "3.4.38", "version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
@@ -1085,6 +1089,7 @@
"version": "5.0.5", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz",
"integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/body-parser": "*", "@types/body-parser": "*",
@@ -1096,6 +1101,7 @@
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz",
"integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
@@ -1108,12 +1114,14 @@
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/jsdom": { "node_modules/@types/jsdom": {
"version": "21.1.7", "version": "21.1.7",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
"integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
@@ -1125,6 +1133,7 @@
"version": "9.0.10", "version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/ms": "*", "@types/ms": "*",
@@ -1142,18 +1151,21 @@
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/ms": { "node_modules/@types/ms": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/multer": { "node_modules/@types/multer": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz",
"integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/express": "*" "@types/express": "*"
@@ -1164,7 +1176,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@@ -1173,18 +1184,21 @@
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/range-parser": { "node_modules/@types/range-parser": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/readdir-glob": { "node_modules/@types/readdir-glob": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz",
"integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
@@ -1194,6 +1208,7 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
@@ -1203,6 +1218,7 @@
"version": "1.15.10", "version": "1.15.10",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
"integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/http-errors": "*", "@types/http-errors": "*",
@@ -1214,6 +1230,7 @@
"version": "0.17.6", "version": "0.17.6",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
"integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/mime": "^1", "@types/mime": "^1",
@@ -1224,6 +1241,7 @@
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz",
"integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"socket.io": "*" "socket.io": "*"
@@ -1257,6 +1275,7 @@
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/trusted-types": { "node_modules/@types/trusted-types": {
@@ -1270,6 +1289,7 @@
"version": "10.0.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vitest/expect": { "node_modules/@vitest/expect": {
@@ -2655,7 +2675,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.0.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.2.0.tgz",
"integrity": "sha512-XdpJDLxfztVY59X0zPI6sibRiGcxhTPXRD3IhJmjKf2jwMvkRGV1j7loB8U+heeamoU3XvihAaGRTR4aXXUN3A==", "integrity": "sha512-XdpJDLxfztVY59X0zPI6sibRiGcxhTPXRD3IhJmjKf2jwMvkRGV1j7loB8U+heeamoU3XvihAaGRTR4aXXUN3A==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.1", "body-parser": "^2.2.1",
@@ -4090,7 +4109,6 @@
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@prisma/engines": "5.22.0" "@prisma/engines": "5.22.0"
}, },
@@ -5103,7 +5121,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -5262,7 +5279,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -5353,7 +5369,6 @@
"integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -5447,7 +5462,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
+9 -9
View File
@@ -1,6 +1,6 @@
{ {
"name": "backend", "name": "backend",
"version": "0.4.0", "version": "0.4.1",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -17,14 +17,6 @@
"type": "commonjs", "type": "commonjs",
"dependencies": { "dependencies": {
"@prisma/client": "^5.22.0", "@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", "archiver": "^7.0.1",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"better-sqlite3": "^12.4.6", "better-sqlite3": "^12.4.6",
@@ -45,10 +37,18 @@
"zod": "^4.1.12" "zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
"@types/archiver": "^7.0.0",
"@types/bcrypt": "^6.0.0",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.5", "@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/node": "^24.10.1",
"@types/socket.io": "^3.0.1",
"@types/supertest": "^6.0.3", "@types/supertest": "^6.0.3",
"@types/uuid": "^10.0.0",
"nodemon": "^3.1.11", "nodemon": "^3.1.11",
"supertest": "^7.1.4", "supertest": "^7.1.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
+3 -1
View File
@@ -6,7 +6,9 @@ services:
- DATABASE_URL=file:/app/prisma/dev.db - DATABASE_URL=file:/app/prisma/dev.db
- PORT=8000 - PORT=8000
- NODE_ENV=production - NODE_ENV=production
# Required for authentication: must be explicitly set to a strong secret (min 32 chars) # Optional for single-instance deployments:
# if unset, backend auto-generates and persists one in the volume.
# Recommended to set explicitly for portability and multi-instance setups.
- JWT_SECRET=${JWT_SECRET} - JWT_SECRET=${JWT_SECRET}
# Required for horizontal scaling (k8s): uncomment and set to same value on all instances # Required for horizontal scaling (k8s): uncomment and set to same value on all instances
# - CSRF_SECRET=${CSRF_SECRET} # - CSRF_SECRET=${CSRF_SECRET}
+3 -1
View File
@@ -8,7 +8,9 @@ services:
- DATABASE_URL=file:/app/prisma/dev.db - DATABASE_URL=file:/app/prisma/dev.db
- PORT=8000 - PORT=8000
- NODE_ENV=production - NODE_ENV=production
# Required for authentication: must be explicitly set to a strong secret (min 32 chars) # Optional for single-instance deployments:
# if unset, backend auto-generates and persists one in the volume.
# Recommended to set explicitly for portability and multi-instance setups.
- JWT_SECRET=${JWT_SECRET} - JWT_SECRET=${JWT_SECRET}
# Required for horizontal scaling (k8s): uncomment and set to same value on all instances # Required for horizontal scaling (k8s): uncomment and set to same value on all instances
# - CSRF_SECRET=${CSRF_SECRET} # - CSRF_SECRET=${CSRF_SECRET}
+1 -4
View File
@@ -7,7 +7,7 @@ WORKDIR /app/frontend
COPY frontend/package*.json ./ COPY frontend/package*.json ./
# Install dependencies # Install dependencies
RUN npm ci RUN npm ci && npm cache clean --force
# Copy source code and config files # Copy source code and config files
COPY frontend/ ./ COPY frontend/ ./
@@ -25,9 +25,6 @@ RUN npm run build
# Production stage # Production stage
FROM nginx:alpine 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 nginx config template (will be processed at runtime)
COPY frontend/nginx.conf.template /etc/nginx/nginx.conf.template COPY frontend/nginx.conf.template /etc/nginx/nginx.conf.template
+3 -3
View File
@@ -7,9 +7,9 @@ export BACKEND_URL="${BACKEND_URL:-backend:8000}"
echo "Configuring nginx with BACKEND_URL: ${BACKEND_URL}" echo "Configuring nginx with BACKEND_URL: ${BACKEND_URL}"
# Substitute environment variables in nginx config template # Replace only our custom placeholder and preserve nginx runtime vars like $http_upgrade
# Only substitute BACKEND_URL, preserve nginx variables like $http_upgrade ESCAPED_BACKEND_URL=$(printf '%s\n' "$BACKEND_URL" | sed 's/[\/&]/\\&/g')
envsubst '${BACKEND_URL}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf sed "s/__BACKEND_URL__/${ESCAPED_BACKEND_URL}/g" /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
# Validate the generated nginx configuration before starting # Validate the generated nginx configuration before starting
echo "Validating nginx configuration..." echo "Validating nginx configuration..."
+2 -2
View File
@@ -24,7 +24,7 @@ http {
# API and WebSocket proxy to backend # API and WebSocket proxy to backend
# BACKEND_URL is substituted at container startup (default: backend:8000) # BACKEND_URL is substituted at container startup (default: backend:8000)
location /api/ { location /api/ {
proxy_pass http://${BACKEND_URL}/; proxy_pass http://__BACKEND_URL__/;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade'; proxy_set_header Connection 'upgrade';
@@ -49,7 +49,7 @@ http {
# WebSocket proxy for Socket.IO # WebSocket proxy for Socket.IO
location /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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade'; proxy_set_header Connection 'upgrade';
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "0.4.0", "version": "0.4.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port 6767", "dev": "vite --port 6767",