Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e6d0d44d7 | |||
| b6d0150d44 | |||
| 55cd816cca | |||
| d67bd1daf8 | |||
| 4b56d3cfc6 | |||
| 88ed4360c0 | |||
| 81918b00cd | |||
| 3b384dc5fb | |||
| 7c238701b7 | |||
| c5c8b15e75 | |||
| 9bc3c7c8fc | |||
| 0476315322 |
@@ -511,6 +511,17 @@ release-docker: ## Build and push release Docker images
|
|||||||
pre-release-docker: ## Build and push pre-release Docker images
|
pre-release-docker: ## Build and push pre-release Docker images
|
||||||
./publish-docker-prerelease.sh
|
./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=<custom-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
|
# DATABASE
|
||||||
#===============================================================================
|
#===============================================================================
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<img src="logoExcaliDash.png" alt="ExcaliDash Logo" width="80" height="88">
|
<img src="logoExcaliDash.png" alt="ExcaliDash Logo" width="80" height="88">
|
||||||
|
|
||||||
# ExcaliDash v0.1.8
|
# ExcaliDash
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
@@ -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:
|
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.
|
- `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
|
```yaml
|
||||||
# docker-compose.yml example
|
# docker-compose.yml example
|
||||||
backend:
|
backend:
|
||||||
environment:
|
environment:
|
||||||
|
# Single URL
|
||||||
- FRONTEND_URL=https://excalidash.example.com
|
- 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:
|
frontend:
|
||||||
environment:
|
environment:
|
||||||
# For standard Docker Compose (default)
|
# For standard Docker Compose (default)
|
||||||
|
|||||||
+14
@@ -27,3 +27,17 @@ CSRF Protection (8a78b2b)
|
|||||||
- Updated docker-compose configurations with new environment variables
|
- Updated docker-compose configurations with new environment variables
|
||||||
- E2E test suite improvements and reliability fixes
|
- E2E test suite improvements and reliability fixes
|
||||||
- Added Kubernetes deployment note in README
|
- Added Kubernetes deployment note in README
|
||||||
|
|
||||||
|
### Kubernetes
|
||||||
|
|
||||||
|
A `CSRF_SECRET` environment variable is now required for CSRF protection. Generate a secure 32+ character random string:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl rand -base64 32
|
||||||
|
|
||||||
|
Add it to your deployment:
|
||||||
|
- Docker Compose: Add CSRF_SECRET=<your-secret> to the backend service environment
|
||||||
|
- Kubernetes: Add to your ConfigMap/Secret and reference in the backend deployment
|
||||||
|
|
||||||
|
If not set, the backend will refuse to start.
|
||||||
|
```
|
||||||
|
|||||||
Generated
+2
-8
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "0.1.8",
|
"version": "0.3.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "0.1.8",
|
"version": "0.3.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
@@ -1128,7 +1128,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"
|
||||||
}
|
}
|
||||||
@@ -3814,7 +3813,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"
|
||||||
},
|
},
|
||||||
@@ -4821,7 +4819,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"
|
||||||
},
|
},
|
||||||
@@ -4980,7 +4977,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"
|
||||||
@@ -5058,7 +5054,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",
|
||||||
@@ -5152,7 +5147,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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "0.2.1",
|
"version": "0.3.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* 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 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,
|
||||||
|
"(not the real client IP)",
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
" 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(
|
||||||
|
" 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
|
||||||
|
// 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.11.166"); // Not the real client IP
|
||||||
|
});
|
||||||
|
});
|
||||||
+37
-3
@@ -129,6 +129,25 @@ const initializeUploadDir = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const app = express();
|
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
|
||||||
|
// 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 httpServer = createServer(app);
|
||||||
const io = new Server(httpServer, {
|
const io = new Server(httpServer, {
|
||||||
cors: {
|
cors: {
|
||||||
@@ -324,9 +343,24 @@ app.use((req, res, next) => {
|
|||||||
const getClientId = (req: express.Request): string => {
|
const getClientId = (req: express.Request): string => {
|
||||||
const ip = req.ip || req.connection.remoteAddress || "unknown";
|
const ip = req.ip || req.connection.remoteAddress || "unknown";
|
||||||
const userAgent = req.headers["user-agent"] || "unknown";
|
const userAgent = req.headers["user-agent"] || "unknown";
|
||||||
// Create a simple hash for client identification
|
const clientId = `${ip}:${userAgent}`.slice(0, 256);
|
||||||
// In production, you might use a session ID instead
|
|
||||||
return `${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
|
// Rate limiter specifically for CSRF token generation to prevent store exhaustion
|
||||||
|
|||||||
@@ -532,7 +532,6 @@ export const validateImportedDrawing = (data: any): boolean => {
|
|||||||
// CSRF Protection
|
// CSRF Protection
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const CSRF_TOKEN_LENGTH = 32;
|
|
||||||
const CSRF_TOKEN_HEADER = "x-csrf-token";
|
const CSRF_TOKEN_HEADER = "x-csrf-token";
|
||||||
const CSRF_TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
|
const CSRF_TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
const CSRF_TOKEN_FUTURE_SKEW_MS = 5 * 60 * 1000; // 5 minutes clock skew tolerance
|
const CSRF_TOKEN_FUTURE_SKEW_MS = 5 * 60 * 1000; // 5 minutes clock skew tolerance
|
||||||
@@ -570,8 +569,12 @@ const getCsrfSecret = (): Buffer => {
|
|||||||
cachedCsrfSecret = crypto.randomBytes(32);
|
cachedCsrfSecret = crypto.randomBytes(32);
|
||||||
const envLabel = process.env.NODE_ENV ? ` (${process.env.NODE_ENV})` : "";
|
const envLabel = process.env.NODE_ENV ? ` (${process.env.NODE_ENV})` : "";
|
||||||
console.warn(
|
console.warn(
|
||||||
`[security] CSRF_SECRET is not set${envLabel}. Using an ephemeral per-process secret. ` +
|
`[SECURITY WARNING] CSRF_SECRET is not set${envLabel}.\n` +
|
||||||
"For horizontal scaling (k8s), set CSRF_SECRET to the same value on all instances."
|
`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=<generated-secret>`
|
||||||
);
|
);
|
||||||
return cachedCsrfSecret;
|
return cachedCsrfSecret;
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+18
-31
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.1.8",
|
"version": "0.3.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.1.8",
|
"version": "0.3.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@excalidraw/excalidraw": "^0.18.0",
|
"@excalidraw/excalidraw": "^0.18.0",
|
||||||
"@types/lodash": "^4.17.20",
|
"@types/lodash": "^4.17.20",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.5",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -162,7 +162,6 @@
|
|||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -517,7 +516,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@@ -561,7 +559,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -2612,7 +2609,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
@@ -2777,7 +2775,6 @@
|
|||||||
"integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==",
|
"integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -2789,7 +2786,6 @@
|
|||||||
"integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
|
"integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
@@ -2860,7 +2856,6 @@
|
|||||||
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
|
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.47.0",
|
"@typescript-eslint/scope-manager": "8.47.0",
|
||||||
"@typescript-eslint/types": "8.47.0",
|
"@typescript-eslint/types": "8.47.0",
|
||||||
@@ -3224,7 +3219,6 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3275,6 +3269,7 @@
|
|||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -3406,13 +3401,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.13.2",
|
"version": "1.13.5",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.11",
|
||||||
"form-data": "^4.0.4",
|
"form-data": "^4.0.5",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3504,7 +3499,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.8.25",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001754",
|
||||||
@@ -3836,7 +3830,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
||||||
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
@@ -4210,7 +4203,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -4450,7 +4442,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
"version": "3.1.6",
|
"version": "3.1.6",
|
||||||
@@ -4669,7 +4662,6 @@
|
|||||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -5503,7 +5495,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.11.0.tgz",
|
||||||
"integrity": "sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==",
|
"integrity": "sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.20.0"
|
"node": ">=12.20.0"
|
||||||
},
|
},
|
||||||
@@ -5851,6 +5842,7 @@
|
|||||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"lz-string": "bin/bin.js"
|
"lz-string": "bin/bin.js"
|
||||||
}
|
}
|
||||||
@@ -6880,7 +6872,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.7",
|
"nanoid": "^3.3.7",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
@@ -7046,6 +7037,7 @@
|
|||||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1",
|
"ansi-regex": "^5.0.1",
|
||||||
"ansi-styles": "^5.0.0",
|
"ansi-styles": "^5.0.0",
|
||||||
@@ -7061,6 +7053,7 @@
|
|||||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@@ -7116,7 +7109,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -7129,7 +7121,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -7143,7 +7134,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
@@ -7858,7 +7850,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"
|
||||||
},
|
},
|
||||||
@@ -7997,7 +7988,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"
|
||||||
@@ -8187,7 +8177,6 @@
|
|||||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||||
"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",
|
||||||
@@ -8281,7 +8270,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"
|
||||||
},
|
},
|
||||||
@@ -8608,7 +8596,6 @@
|
|||||||
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
|
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.1",
|
"version": "0.3.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@excalidraw/excalidraw": "^0.18.0",
|
"@excalidraw/excalidraw": "^0.18.0",
|
||||||
"@types/lodash": "^4.17.20",
|
"@types/lodash": "^4.17.20",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.5",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const UploadProvider: React.FC<{ children: ReactNode }> = ({ children })
|
|||||||
|
|
||||||
const uploadFiles = useCallback(async (files: File[], targetCollectionId: string | null) => {
|
const uploadFiles = useCallback(async (files: File[], targetCollectionId: string | null) => {
|
||||||
const newTasks: UploadTask[] = files.map(f => ({
|
const newTasks: UploadTask[] = files.map(f => ({
|
||||||
id: Math.random().toString(36).substring(2, 11),
|
id: crypto.randomUUID(),
|
||||||
fileName: f.name,
|
fileName: f.name,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
progress: 0
|
progress: 0
|
||||||
@@ -56,12 +56,12 @@ export const UploadProvider: React.FC<{ children: ReactNode }> = ({ children })
|
|||||||
|
|
||||||
setTasks(prev => [...newTasks, ...prev]);
|
setTasks(prev => [...newTasks, ...prev]);
|
||||||
|
|
||||||
// Map file names to task IDs for progress callbacks
|
// Map file index to task ID for progress callbacks (handles duplicate filenames)
|
||||||
const fileTaskMap = new Map<string, string>();
|
const indexToTaskId = new Map<number, string>();
|
||||||
newTasks.forEach(t => fileTaskMap.set(t.fileName, t.id));
|
newTasks.forEach((t, index) => indexToTaskId.set(index, t.id));
|
||||||
|
|
||||||
const handleProgress = (fileName: string, status: UploadStatus, progress: number, error?: string) => {
|
const handleProgress = (fileIndex: number, status: UploadStatus, progress: number, error?: string) => {
|
||||||
const taskId = fileTaskMap.get(fileName);
|
const taskId = indexToTaskId.get(fileIndex);
|
||||||
if (taskId) {
|
if (taskId) {
|
||||||
updateTask(taskId, { status, progress, error });
|
updateTask(taskId, { status, progress, error });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const importDrawings = async (
|
|||||||
targetCollectionId: string | null,
|
targetCollectionId: string | null,
|
||||||
onSuccess?: () => void | Promise<void>,
|
onSuccess?: () => void | Promise<void>,
|
||||||
onProgress?: (
|
onProgress?: (
|
||||||
fileName: string,
|
fileIndex: number,
|
||||||
status: UploadStatus,
|
status: UploadStatus,
|
||||||
progress: number,
|
progress: number,
|
||||||
error?: string
|
error?: string
|
||||||
@@ -25,12 +25,20 @@ export const importDrawings = async (
|
|||||||
let failCount = 0;
|
let failCount = 0;
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// Build a map from drawingFile index to original file index for progress reporting
|
||||||
|
const originalIndexMap = new Map<number, number>();
|
||||||
|
drawingFiles.forEach((df, i) => {
|
||||||
|
const originalIndex = files.indexOf(df);
|
||||||
|
originalIndexMap.set(i, originalIndex);
|
||||||
|
});
|
||||||
|
|
||||||
// We process files in parallel (Promise.all) but we could limit concurrency if needed.
|
// We process files in parallel (Promise.all) but we could limit concurrency if needed.
|
||||||
// For now, full parallel is fine as browser limits connection count anyway.
|
// For now, full parallel is fine as browser limits connection count anyway.
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
drawingFiles.map(async (file) => {
|
drawingFiles.map(async (file, drawingIndex) => {
|
||||||
|
const fileIndex = originalIndexMap.get(drawingIndex) ?? drawingIndex;
|
||||||
try {
|
try {
|
||||||
if (onProgress) onProgress(file.name, 'processing', 0); // Parsing phase
|
if (onProgress) onProgress(fileIndex, 'processing', 0); // Parsing phase
|
||||||
|
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
const data = JSON.parse(text);
|
const data = JSON.parse(text);
|
||||||
@@ -61,7 +69,7 @@ export const importDrawings = async (
|
|||||||
preview: svg.outerHTML,
|
preview: svg.outerHTML,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (onProgress) onProgress(file.name, 'uploading', 0);
|
if (onProgress) onProgress(fileIndex, 'uploading', 0);
|
||||||
|
|
||||||
await api.post("/drawings", payload, {
|
await api.post("/drawings", payload, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -73,12 +81,12 @@ export const importDrawings = async (
|
|||||||
const percentCompleted = Math.round(
|
const percentCompleted = Math.round(
|
||||||
(progressEvent.loaded * 100) / progressEvent.total
|
(progressEvent.loaded * 100) / progressEvent.total
|
||||||
);
|
);
|
||||||
onProgress(file.name, 'uploading', percentCompleted);
|
onProgress(fileIndex, 'uploading', percentCompleted);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (onProgress) onProgress(file.name, 'success', 100);
|
if (onProgress) onProgress(fileIndex, 'success', 100);
|
||||||
successCount++;
|
successCount++;
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -90,7 +98,7 @@ export const importDrawings = async (
|
|||||||
err?.message ||
|
err?.message ||
|
||||||
"Upload failed";
|
"Upload failed";
|
||||||
errors.push(`${file.name}: ${errorMessage}`);
|
errors.push(`${file.name}: ${errorMessage}`);
|
||||||
if (onProgress) onProgress(file.name, 'error', 0, errorMessage);
|
if (onProgress) onProgress(fileIndex, 'error', 0, errorMessage);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,19 +15,16 @@ try {
|
|||||||
console.warn("Unable to read VERSION file:", error);
|
console.warn("Unable to read VERSION file:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const appVersion = process.env.VITE_APP_VERSION?.trim() || versionFromFile;
|
||||||
!process.env.VITE_APP_VERSION ||
|
const buildLabel = process.env.VITE_APP_BUILD_LABEL?.trim() || "local development build";
|
||||||
process.env.VITE_APP_VERSION.trim().length === 0
|
|
||||||
) {
|
|
||||||
process.env.VITE_APP_VERSION = versionFromFile;
|
|
||||||
if (!process.env.VITE_APP_BUILD_LABEL) {
|
|
||||||
process.env.VITE_APP_BUILD_LABEL = "local development build";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
define: {
|
||||||
|
'import.meta.env.VITE_APP_VERSION': JSON.stringify(appVersion),
|
||||||
|
'import.meta.env.VITE_APP_BUILD_LABEL': JSON.stringify(buildLabel),
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
|
|||||||
Executable
+109
@@ -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 <custom-name>${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 ""
|
||||||
Reference in New Issue
Block a user