From 88ed4360c022967e54d4652bf1f8c2f97b0ddb72 Mon Sep 17 00:00:00 2001 From: Zimeng Xiong Date: Sun, 1 Feb 2026 16:00:44 -0800 Subject: [PATCH 1/5] docs: document comma-separated FRONTEND_URL support Clarifies that FRONTEND_URL accepts multiple comma-separated URLs for accessing ExcaliDash from different addresses (e.g., localhost and LAN IP simultaneously). --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c2510b..6b566c0 100644 --- a/README.md +++ b/README.md @@ -120,14 +120,17 @@ docker compose up -d When running ExcaliDash behind Traefik, Nginx, or another reverse proxy, configure both containers so that API + WebSocket calls resolve correctly: -- `FRONTEND_URL` (backend) must match the public URL that users hit (e.g. `https://excalidash.example.com`). This controls CORS and Socket.IO origin checks. +- `FRONTEND_URL` (backend) must match the public URL that users hit (e.g. `https://excalidash.example.com`). This controls CORS and Socket.IO origin checks. **Supports multiple comma-separated URLs** for accessing from different addresses. - `BACKEND_URL` (frontend) tells the Nginx container how to reach the backend from inside Docker/Kubernetes. Override it if your reverse proxy exposes the backend under a different hostname. ```yaml # docker-compose.yml example backend: environment: + # Single URL - FRONTEND_URL=https://excalidash.example.com + # Or multiple URLs (comma-separated) for local + network access + # - FRONTEND_URL=http://localhost:6767,http://192.168.1.100:6767,http://nas.local:6767 frontend: environment: # For standard Docker Compose (default) From 4b56d3cfc66b830049545d5b8edc3f4f4a7181f5 Mon Sep 17 00:00:00 2001 From: Zimeng Xiong Date: Fri, 30 Jan 2026 14:11:04 -0800 Subject: [PATCH 2/5] repro issue --- Makefile | 11 ++ backend/package-lock.json | 10 +- .../src/__tests__/csrf-trust-proxy.test.ts | 171 ++++++++++++++++++ publish-docker-dev.sh | 109 +++++++++++ 4 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 backend/src/__tests__/csrf-trust-proxy.test.ts create mode 100755 publish-docker-dev.sh diff --git a/Makefile b/Makefile index f216285..db271b8 100644 --- a/Makefile +++ b/Makefile @@ -511,6 +511,17 @@ release-docker: ## Build and push release Docker images pre-release-docker: ## Build and push pre-release Docker images ./publish-docker-prerelease.sh +dev-release: ## Build and push custom dev release (usage: make dev-release NAME=issue38) + @if [ -z "$(NAME)" ]; then \ + echo "$(RED)ERROR: NAME parameter is required!$(NC)"; \ + echo "$(YELLOW)Usage: make dev-release NAME=$(NC)"; \ + echo "$(YELLOW)Example: make dev-release NAME=issue38$(NC)"; \ + echo "$(YELLOW) This will create tags like: 0.3.1-dev-issue38$(NC)"; \ + exit 1; \ + fi + @echo "$(BLUE)Building custom dev release: $(NAME)$(NC)" + @./publish-docker-dev.sh $(NAME) + #=============================================================================== # DATABASE #=============================================================================== diff --git a/backend/package-lock.json b/backend/package-lock.json index 10b57c8..0f3ec73 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "backend", - "version": "0.1.8", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "backend", - "version": "0.1.8", + "version": "0.3.1", "license": "ISC", "dependencies": { "@prisma/client": "^5.22.0", @@ -1128,7 +1128,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" } @@ -3814,7 +3813,6 @@ "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/engines": "5.22.0" }, @@ -4821,7 +4819,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4980,7 +4977,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5058,7 +5054,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", @@ -5152,7 +5147,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/backend/src/__tests__/csrf-trust-proxy.test.ts b/backend/src/__tests__/csrf-trust-proxy.test.ts new file mode 100644 index 0000000..3bdf634 --- /dev/null +++ b/backend/src/__tests__/csrf-trust-proxy.test.ts @@ -0,0 +1,171 @@ +/** + * Issue #38: CSRF fails with multiple reverse proxies + * + * This test demonstrates how trust proxy settings affect CSRF validation + * when ExcaliDash is behind multiple proxy layers (e.g., Traefik, Synology NAS) + */ + +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import express from "express"; +import request from "supertest"; +import { + createCsrfToken, + validateCsrfToken, + getCsrfTokenHeader, +} from "../security"; + +// mock the getClientId function behavior +const getClientIdFromRequest = (req: express.Request): string => { + const ip = req.ip || req.connection.remoteAddress || "unknown"; + const userAgent = req.headers["user-agent"] || "unknown"; + return `${ip}:${userAgent}`.slice(0, 256); +}; + +describe("Issue #38: CSRF with trust proxy settings", () => { + let app: express.Application; + + beforeEach(() => { + app = express(); + app.use(express.json()); + }); + + it("demonstrates the trust proxy issue with multiple proxies", async () => { + // ext proxy -> frontend nginx -> backend + // X-Forwarded-For: 203.0.113.42 (client), 10.0.0.5 (external proxy), 172.17.0.3 (frontend nginx) + + // With trust proxy: 1 (current setting) + const app1 = express(); + app1.set("trust proxy", 1); + app1.use(express.json()); + + app1.get("/test-ip", (req, res) => { + res.json({ + ip: req.ip, + clientId: getClientIdFromRequest(req), + }); + }); + + // Simulate request through multiple proxies + const response1 = await request(app1) + .get("/test-ip") + .set("X-Forwarded-For", "203.0.113.42, 10.0.0.5, 172.17.0.3") + .set("User-Agent", "Mozilla/5.0 Test"); + + // With trust proxy: 1, Express takes second-to-last IP (the external proxy) + expect(response1.body.ip).toBe("10.0.0.5"); + console.log( + "trust proxy: 1 → IP:", + response1.body.ip, + "(external proxy IP - WRONG)", + ); + + // With trust proxy: true + const app2 = express(); + app2.set("trust proxy", true); + app2.use(express.json()); + + app2.get("/test-ip", (req, res) => { + res.json({ + ip: req.ip, + clientId: getClientIdFromRequest(req), + }); + }); + + const response2 = await request(app2) + .get("/test-ip") + .set("X-Forwarded-For", "203.0.113.42, 10.0.0.5, 172.17.0.3") + .set("User-Agent", "Mozilla/5.0 Test"); + + // With trust proxy: true, Express takes leftmost IP + expect(response2.body.ip).toBe("203.0.113.42"); + console.log( + "trust proxy: true → IP:", + response2.body.ip, + "(real client IP - CORRECT)", + ); + }); + + it("simulates CSRF failure scenario from issue #38", async () => { + const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"; + + // Request 1: Fetch CSRF token + // X-Forwarded-For shows: client, external-proxy-1, frontend-nginx + const clientIp1 = "203.0.113.42"; + const externalProxyIp1 = "10.0.0.5"; // External proxy IP on first request + + // With trust proxy: 1, Express sees the external proxy IP + const clientId1 = `${externalProxyIp1}:${userAgent}`; + const token = createCsrfToken(clientId1); + + console.log("\nšŸ“ Step 1: Client fetches CSRF token"); + console.log( + " X-Forwarded-For:", + `${clientIp1}, ${externalProxyIp1}, 172.17.0.3`, + ); + console.log(" Express sees IP:", externalProxyIp1); + console.log(" ClientId:", clientId1.slice(0, 50) + "..."); + + // Request 2: Try to create drawing with token + // External proxy IP might differ slightly + const externalProxyIp2 = "10.0.0.6"; + + const clientId2 = `${externalProxyIp2}:${userAgent}`; + + console.log("\nšŸ“¤ Step 2: Client creates drawing with token"); + console.log( + " X-Forwarded-For:", + `${clientIp1}, ${externalProxyIp2}, 172.17.0.3`, + ); + console.log(" Express sees IP:", externalProxyIp2); + console.log(" ClientId:", clientId2.slice(0, 50) + "..."); + + // CSRF validation fails because clientId changed + const isValid = validateCsrfToken(clientId2, token); + + expect(isValid).toBe(false); + console.log(" Expected:", clientId1.slice(0, 50) + "..."); + console.log(" Got:", clientId2.slice(0, 50) + "..."); + }); + + it("shows the fix works with trust proxy: true", async () => { + const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"; + const realClientIp = "203.0.113.42"; + + const clientId1 = `${realClientIp}:${userAgent}`; + const token = createCsrfToken(clientId1); + + console.log(" X-Forwarded-For:", `${realClientIp}, 10.0.0.5, 172.17.0.3`); + console.log(" Express sees IP:", realClientIp); + + // Request 2: Use token (even if middle proxy IPs differ) + const clientId2 = `${realClientIp}:${userAgent}`; + + console.log("Create drawing"); + console.log("X-Forwarded-For:", `${realClientIp}, 10.0.0.6, 172.17.0.3`); + console.log("Express sees IP:", realClientIp, "(same!)"); + + const isValid = validateCsrfToken(clientId2, token); + + expect(isValid).toBe(true); + console.log("\nCSRF Validation: SUCCESS"); + }); + + it("demonstrates the Synology NAS scenario from issue #38", async () => { + const app = express(); + app.set("trust proxy", 1); + app.use(express.json()); + + let seenIp: string | undefined; + app.get("/test", (req, res) => { + seenIp = req.ip; + res.json({ ip: req.ip }); + }); + + // Client -> Synology (192.168.1.x) -> Docker frontend (192.168.11.x) -> Backend + await request(app) + .get("/test") + .set("X-Forwarded-For", "192.168.0.100, 192.168.1.4, 192.168.11.166"); + console.log(" With trust proxy: 1, Express sees:", seenIp); + expect(seenIp).toBe("192.168.1.4"); // Proxy IP, not client IP + }); +}); diff --git a/publish-docker-dev.sh b/publish-docker-dev.sh new file mode 100755 index 0000000..679d8a9 --- /dev/null +++ b/publish-docker-dev.sh @@ -0,0 +1,109 @@ +#!/bin/bash +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Custom name is required +CUSTOM_NAME=$1 + +if [ -z "$CUSTOM_NAME" ]; then + echo -e "${RED}ERROR: Custom name is required!${NC}" + echo -e "${YELLOW}Usage: $0 ${NC}" + echo -e "${YELLOW}Example: $0 issue38${NC}" + echo -e "${YELLOW} This will create tags like: 0.3.1-dev-issue38${NC}" + exit 1 +fi + +# Configuration +DOCKER_USERNAME="zimengxiong" +IMAGE_NAME="excalidash" +BASE_VERSION=$(node -e "try { console.log(require('fs').readFileSync('VERSION', 'utf8').trim()) } catch { console.log('0.0.0') }") +VERSION="${BASE_VERSION}-dev-${CUSTOM_NAME}" +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + +echo -e "${BLUE}===========================================${NC}" +echo -e "${BLUE}ExcaliDash Custom Dev Release${NC}" +echo -e "${BLUE}===========================================${NC}" +echo "" +echo -e "${YELLOW}Branch: ${CURRENT_BRANCH}${NC}" +echo -e "${YELLOW}Base version: ${BASE_VERSION}${NC}" +echo -e "${YELLOW}Custom name: ${CUSTOM_NAME}${NC}" +echo -e "${YELLOW}Full tag: ${VERSION}${NC}" +echo "" +echo -e "${YELLOW}This will publish images with tag: ${VERSION}${NC}" +echo -e "${YELLOW}Dev images will NOT update 'latest' or 'dev' tags${NC}" +echo "" + +# Confirm before proceeding +read -p "Continue? (y/N) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo -e "${RED}Aborted.${NC}" + exit 1 +fi + +# Check if logged in to Docker Hub +echo -e "${YELLOW}Checking Docker Hub authentication...${NC}" +if ! docker info | grep -q "Username: $DOCKER_USERNAME"; then + echo -e "${YELLOW}Not logged in. Please login to Docker Hub:${NC}" + docker login +else + echo -e "${GREEN}āœ“ Already logged in as $DOCKER_USERNAME${NC}" +fi + +# Create buildx builder if it doesn't exist +echo -e "${YELLOW}Setting up buildx builder...${NC}" +if ! docker buildx inspect excalidash-builder > /dev/null 2>&1; then + echo -e "${YELLOW}Creating new buildx builder...${NC}" + docker buildx create --name excalidash-builder --use --bootstrap +else + echo -e "${GREEN}āœ“ Using existing buildx builder${NC}" + docker buildx use excalidash-builder +fi + +# Build and push backend image +echo "" +echo -e "${BLUE}Building and pushing backend image...${NC}" +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --tag $DOCKER_USERNAME/$IMAGE_NAME-backend:$VERSION \ + --file backend/Dockerfile \ + --push \ + backend/ + +echo -e "${GREEN}āœ“ Backend image pushed successfully${NC}" + +# Build and push frontend image +echo "" +echo -e "${BLUE}Building and pushing frontend image...${NC}" +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --tag $DOCKER_USERNAME/$IMAGE_NAME-frontend:$VERSION \ + --build-arg VITE_APP_VERSION=$VERSION \ + --file frontend/Dockerfile \ + --push \ + . + +echo -e "${GREEN}āœ“ Frontend image pushed successfully${NC}" + +echo "" +echo -e "${BLUE}===========================================${NC}" +echo -e "${GREEN}āœ“ Custom dev images published!${NC}" +echo -e "${BLUE}===========================================${NC}" +echo "" +echo -e "${YELLOW}Images published:${NC}" +echo -e " • $DOCKER_USERNAME/$IMAGE_NAME-backend:$VERSION" +echo -e " • $DOCKER_USERNAME/$IMAGE_NAME-frontend:$VERSION" +echo "" +echo -e "${YELLOW}To use these images in docker-compose:${NC}" +echo -e "${BLUE} services:" +echo -e " backend:" +echo -e " image: $DOCKER_USERNAME/$IMAGE_NAME-backend:$VERSION" +echo -e " frontend:" +echo -e " image: $DOCKER_USERNAME/$IMAGE_NAME-frontend:$VERSION${NC}" +echo "" From d67bd1daf88086019ae369d0d90f03e5fcadfb6f Mon Sep 17 00:00:00 2001 From: Zimeng Xiong Date: Fri, 30 Jan 2026 14:26:03 -0800 Subject: [PATCH 3/5] fix express proxy headers --- .../src/__tests__/csrf-trust-proxy.test.ts | 2 - backend/src/index.ts | 38 ++++++++++++++++--- backend/src/security.ts | 8 +++- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/backend/src/__tests__/csrf-trust-proxy.test.ts b/backend/src/__tests__/csrf-trust-proxy.test.ts index 3bdf634..021994f 100644 --- a/backend/src/__tests__/csrf-trust-proxy.test.ts +++ b/backend/src/__tests__/csrf-trust-proxy.test.ts @@ -97,7 +97,6 @@ describe("Issue #38: CSRF with trust proxy settings", () => { const clientId1 = `${externalProxyIp1}:${userAgent}`; const token = createCsrfToken(clientId1); - console.log("\nšŸ“ Step 1: Client fetches CSRF token"); console.log( " X-Forwarded-For:", `${clientIp1}, ${externalProxyIp1}, 172.17.0.3`, @@ -111,7 +110,6 @@ describe("Issue #38: CSRF with trust proxy settings", () => { const clientId2 = `${externalProxyIp2}:${userAgent}`; - console.log("\nšŸ“¤ Step 2: Client creates drawing with token"); console.log( " X-Forwarded-For:", `${clientIp1}, ${externalProxyIp2}, 172.17.0.3`, diff --git a/backend/src/index.ts b/backend/src/index.ts index 0a4a773..65adce9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -132,8 +132,21 @@ const app = express(); // Trust proxy headers (X-Forwarded-For, X-Real-IP) from nginx // Required for correct client IP detection when running behind a reverse proxy -// This fixes CSRF token validation failures in Docker/K8s environments -app.set("trust proxy", 1); +// Fix for issue #38: Use 'true' to handle multiple proxy layers (e.g., Traefik, Synology NAS) +// This ensures Express extracts the real client IP from the leftmost X-Forwarded-For value +const trustProxyConfig = process.env.TRUST_PROXY || "true"; +const trustProxyValue = trustProxyConfig === "true" + ? true + : trustProxyConfig === "false" + ? false + : parseInt(trustProxyConfig, 10) || 1; +app.set("trust proxy", trustProxyValue); + +if (trustProxyValue === true) { + console.log("[config] trust proxy: enabled (handles multiple proxy layers)"); +} else { + console.log(`[config] trust proxy: ${trustProxyValue}`); +} const httpServer = createServer(app); const io = new Server(httpServer, { @@ -330,9 +343,24 @@ app.use((req, res, next) => { const getClientId = (req: express.Request): string => { const ip = req.ip || req.connection.remoteAddress || "unknown"; const userAgent = req.headers["user-agent"] || "unknown"; - // Create a simple hash for client identification - // In production, you might use a session ID instead - return `${ip}:${userAgent}`.slice(0, 256); + const clientId = `${ip}:${userAgent}`.slice(0, 256); + + // Debug logging for CSRF troubleshooting (issue #38) + if (process.env.DEBUG_CSRF === "true") { + console.log("[CSRF DEBUG] getClientId", { + method: req.method, + path: req.path, + ip, + remoteAddress: req.connection.remoteAddress, + "x-forwarded-for": req.headers["x-forwarded-for"], + "x-real-ip": req.headers["x-real-ip"], + userAgent: userAgent.slice(0, 100), + clientIdPreview: clientId.slice(0, 60) + "...", + trustProxySetting: req.app.get("trust proxy"), + }); + } + + return clientId; }; // Rate limiter specifically for CSRF token generation to prevent store exhaustion diff --git a/backend/src/security.ts b/backend/src/security.ts index 87287ed..bd90c33 100644 --- a/backend/src/security.ts +++ b/backend/src/security.ts @@ -569,8 +569,12 @@ const getCsrfSecret = (): Buffer => { cachedCsrfSecret = crypto.randomBytes(32); const envLabel = process.env.NODE_ENV ? ` (${process.env.NODE_ENV})` : ""; console.warn( - `[security] CSRF_SECRET is not set${envLabel}. Using an ephemeral per-process secret. ` + - "For horizontal scaling (k8s), set CSRF_SECRET to the same value on all instances." + `[SECURITY WARNING] CSRF_SECRET is not set${envLabel}.\n` + + `Using an ephemeral per-process secret.\n` + + ` - Tokens will expire on container restart\n` + + ` - Horizontal scaling (k8s) will NOT work\n` + + ` - Generate a secret: openssl rand -base64 32\n` + + ` - Set environment variable: CSRF_SECRET=` ); return cachedCsrfSecret; }; From 55cd816ccad8176d0f058349de706b5738c8fefc Mon Sep 17 00:00:00 2001 From: Zimeng Xiong Date: Sun, 1 Feb 2026 16:05:45 -0800 Subject: [PATCH 4/5] fix: correct test assertions for trust proxy behavior in supertest The demonstration tests had incorrect assumptions about how Express trust proxy works in supertest (no real socket connection). Updated assertions to match actual behavior while preserving the test's purpose of showing that trust proxy: true extracts the correct client IP. --- backend/src/__tests__/csrf-trust-proxy.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/src/__tests__/csrf-trust-proxy.test.ts b/backend/src/__tests__/csrf-trust-proxy.test.ts index 021994f..beadca9 100644 --- a/backend/src/__tests__/csrf-trust-proxy.test.ts +++ b/backend/src/__tests__/csrf-trust-proxy.test.ts @@ -51,12 +51,13 @@ describe("Issue #38: CSRF with trust proxy settings", () => { .set("X-Forwarded-For", "203.0.113.42, 10.0.0.5, 172.17.0.3") .set("User-Agent", "Mozilla/5.0 Test"); - // With trust proxy: 1, Express takes second-to-last IP (the external proxy) - expect(response1.body.ip).toBe("10.0.0.5"); + // With trust proxy: 1 in supertest (no real socket), Express takes the last IP + // In production with a real connection, behavior differs - the key point is it's NOT the client IP + expect(response1.body.ip).toBe("172.17.0.3"); console.log( "trust proxy: 1 → IP:", response1.body.ip, - "(external proxy IP - WRONG)", + "(not the real client IP)", ); // With trust proxy: true @@ -160,10 +161,12 @@ describe("Issue #38: CSRF with trust proxy settings", () => { }); // Client -> Synology (192.168.1.x) -> Docker frontend (192.168.11.x) -> Backend + // In supertest without real socket, trust proxy: 1 returns last IP + // Key point: it's NOT the real client IP (192.168.0.100) await request(app) .get("/test") .set("X-Forwarded-For", "192.168.0.100, 192.168.1.4, 192.168.11.166"); console.log(" With trust proxy: 1, Express sees:", seenIp); - expect(seenIp).toBe("192.168.1.4"); // Proxy IP, not client IP + expect(seenIp).toBe("192.168.11.166"); // Not the real client IP }); }); From b6d0150d441b1b4dc1aa156db7ffcea74f668770 Mon Sep 17 00:00:00 2001 From: Zimeng Xiong Date: Sun, 1 Feb 2026 16:06:19 -0800 Subject: [PATCH 5/5] chore: release v0.3.2 --- VERSION | 2 +- backend/package.json | 2 +- frontend/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 9e11b32..d15723f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.1 +0.3.2 diff --git a/backend/package.json b/backend/package.json index f75a26c..bb277ae 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "backend", - "version": "0.3.1", + "version": "0.3.2", "description": "", "main": "index.js", "scripts": { diff --git a/frontend/package.json b/frontend/package.json index 221bf41..38013e0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.3.1", + "version": "0.3.2", "type": "module", "scripts": { "dev": "vite",