From 7f78e70a35eb99f928f92b23be90f9e22b2a1fca Mon Sep 17 00:00:00 2001 From: Davidutz_ <71325082+DavidutzDev@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:53:49 +0100 Subject: [PATCH 1/6] fix: make prisma provider configurable by env DATABASE_PROVIDER (#71) --- backend/prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 3e2bb588..c3ab2425 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -5,7 +5,7 @@ generator client { } datasource db { - provider = "sqlite" + provider = env("DATABASE_PROVIDER") url = env("DATABASE_URL") } From be21e8323ccc2a2e5efb04dcaf658e2ba9fbfec5 Mon Sep 17 00:00:00 2001 From: Davidutz_ <71325082+DavidutzDev@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:58:36 +0100 Subject: [PATCH 2/6] docs: Documenting the env var for DATABASE_PROVIDER (#71) --- backend/.env.example | 1 + backend/vitest.config.ts | 1 + docker-compose.prod.yml | 1 + docker-compose.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/backend/.env.example b/backend/.env.example index aae43fff..455c32ac 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,6 +1,7 @@ # Backend Environment Variables PORT=8000 NODE_ENV=production +DATABASE_PROVIDER=sqlite DATABASE_URL=file:/app/prisma/dev.db FRONTEND_URL=http://localhost:6767 # Keep disabled unless traffic always comes through a trusted reverse proxy. diff --git a/backend/vitest.config.ts b/backend/vitest.config.ts index 728ed29a..0e50c81d 100644 --- a/backend/vitest.config.ts +++ b/backend/vitest.config.ts @@ -8,6 +8,7 @@ export default defineConfig({ testTimeout: 30000, hookTimeout: 30000, env: { + DATABASE_PROVIDER: "sqlite", DATABASE_URL: "file:./prisma/test.db", NODE_ENV: "test", AUTH_MODE: "local", diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 0a6a8f76..71e8d22b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -3,6 +3,7 @@ services: image: zimengxiong/excalidash-backend:latest container_name: excalidash-backend environment: + - DATABASE_PROVIDER=sqlite - DATABASE_URL=file:/app/prisma/dev.db - PORT=8000 - NODE_ENV=production diff --git a/docker-compose.yml b/docker-compose.yml index bc75cc3f..5294f109 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,7 @@ services: dockerfile: Dockerfile container_name: excalidash-backend environment: + - DATABASE_PROVIDER=sqlite - DATABASE_URL=file:/app/prisma/dev.db - PORT=8000 - NODE_ENV=production From acd5e94acdc0f807ca293d5e880d5a763d82078e Mon Sep 17 00:00:00 2001 From: Davidutz_ <71325082+DavidutzDev@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:42:05 +0100 Subject: [PATCH 3/6] feat(docker): add DATABASE_PROVIDER env var support for runtime provider switching Allows container to use sqlite (default) or postgresql by setting DATABASE_PROVIDER and DATABASE_URL environment variables at runtime. --- backend/Dockerfile | 8 ++++++-- backend/docker-entrypoint.sh | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index e50b0eec..bbeb1b22 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -16,7 +16,11 @@ RUN npm ci && npm cache clean --force # Copy prisma schema COPY prisma ./prisma/ -# Generate Prisma Client +# Set default provider for build-time Prisma generation (runtime can override via DATABASE_PROVIDER) +RUN sed -i 's/provider = env("[^"]*")/provider = "sqlite"/' prisma/schema.prisma + +# Generate Prisma Client at build time (for TypeScript compilation) +# Runtime will regenerate if DATABASE_PROVIDER differs RUN npx prisma generate # Copy source code @@ -51,7 +55,7 @@ COPY prisma ./prisma_template/ # Copy built application from builder 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 (will be regenerated at runtime if provider changes) COPY --from=builder /app/src/generated ./dist/generated # Create necessary directories (ownership will be set in entrypoint) diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh index 894073a1..9e94fea2 100644 --- a/backend/docker-entrypoint.sh +++ b/backend/docker-entrypoint.sh @@ -64,6 +64,25 @@ mkdir -p /app/prisma/migrations cp /app/prisma_template/schema.prisma /app/prisma/schema.prisma cp -R /app/prisma_template/migrations/. /app/prisma/migrations/ +# Set default DATABASE_PROVIDER if not set +if [ -z "${DATABASE_PROVIDER:-}" ]; then + echo "DATABASE_PROVIDER not set, defaulting to sqlite" + DATABASE_PROVIDER="sqlite" +fi + +# Update schema.prisma with the runtime provider (handles both env() and static values) +echo "Configuring Prisma for provider: ${DATABASE_PROVIDER}" +sed -i 's/provider = env("[^"]*")/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/schema.prisma +sed -i 's/provider = "[^"]*"/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/schema.prisma + +# Generate Prisma Client at runtime (run as root since schema is owned by root) +echo "Generating Prisma Client..." +npx prisma generate --schema=/app/prisma/schema.prisma + +# Copy generated client to the expected location for the application +mkdir -p /app/dist/generated +cp -r /app/src/generated/* /app/dist/generated/ + # 2. Fix permissions unconditionally (Running as root) echo "Fixing filesystem permissions..." chown -R nodejs:nodejs /app/uploads From 1c2a1c9b0c37f03a0dd25091501ad4dc3d164735 Mon Sep 17 00:00:00 2001 From: Davidutz_ <71325082+DavidutzDev@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:13:18 +0100 Subject: [PATCH 4/6] fix: I'm not sure how to deal this... Prisma is really annoying on databases provider and it's not really a good idea to do like this. This should only be a temporary fix --- backend/docker-entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh index 9e94fea2..eec234f6 100644 --- a/backend/docker-entrypoint.sh +++ b/backend/docker-entrypoint.sh @@ -74,6 +74,7 @@ fi echo "Configuring Prisma for provider: ${DATABASE_PROVIDER}" sed -i 's/provider = env("[^"]*")/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/schema.prisma sed -i 's/provider = "[^"]*"/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/schema.prisma +sed -i 's/^provider = ".*"/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/migrations/migration_lock.toml # Generate Prisma Client at runtime (run as root since schema is owned by root) echo "Generating Prisma Client..." From 5d3e19ba37259c6a31de1828871f2f4a360c3e9a Mon Sep 17 00:00:00 2001 From: Davidutz_ <71325082+DavidutzDev@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:51:52 +0100 Subject: [PATCH 5/6] fix: Better implementation. Instead of messing with some experimental stuff. Now we just create a separate migrations folder for postgres and update docker --- README.md | 77 +++++- backend/.env.example | 1 + backend/docker-entrypoint.sh | 36 +-- backend/package-lock.json | 11 +- backend/prisma.config.ts | 12 - .../20260225171236_init/migration.sql | 233 ++++++++++++++++++ .../migrations/postgresql/migration_lock.toml | 3 + .../20251122021659_init/migration.sql | 0 .../20251122032308_add_preview/migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../{ => sqlite}/migration_lock.toml | 0 docker-compose.pg-test.yml | 43 ++++ scripts/generate-migrations.sh | 147 +++++++++++ 23 files changed, 523 insertions(+), 40 deletions(-) delete mode 100644 backend/prisma.config.ts create mode 100644 backend/prisma/migrations/postgresql/20260225171236_init/migration.sql create mode 100644 backend/prisma/migrations/postgresql/migration_lock.toml rename backend/prisma/migrations/{ => sqlite}/20251122021659_init/migration.sql (100%) rename backend/prisma/migrations/{ => sqlite}/20251122032308_add_preview/migration.sql (100%) rename backend/prisma/migrations/{ => sqlite}/20251122065455_add_files_column/migration.sql (100%) rename backend/prisma/migrations/{ => sqlite}/20251124220546_add_library_model/migration.sql (100%) rename backend/prisma/migrations/{ => sqlite}/20260124145151_add_user_auth/migration.sql (100%) rename backend/prisma/migrations/{ => sqlite}/20260124152839_add_password_reset_audit_refresh_tokens/migration.sql (100%) rename backend/prisma/migrations/{ => sqlite}/20260206173000_add_auth_enabled_system_config/migration.sql (100%) rename backend/prisma/migrations/{ => sqlite}/20260206195000_add_auth_login_rate_limit_config/migration.sql (100%) rename backend/prisma/migrations/{ => sqlite}/20260207000000_add_query_indexes/migration.sql (100%) rename backend/prisma/migrations/{ => sqlite}/20260210153000_add_auth_onboarding_completed/migration.sql (100%) rename backend/prisma/migrations/{ => sqlite}/20260210190000_add_auth_identity/migration.sql (100%) rename backend/prisma/migrations/{ => sqlite}/20260211232000_add_bootstrap_setup_code/migration.sql (100%) rename backend/prisma/migrations/{ => sqlite}/20260217214759_drawing_sharing/migration.sql (100%) rename backend/prisma/migrations/{ => sqlite}/migration_lock.toml (100%) create mode 100644 docker-compose.pg-test.yml create mode 100755 scripts/generate-migrations.sh diff --git a/README.md b/README.md index 73564c1e..def2221d 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ Why: | Area | Limitation | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Database | The backend uses a local **SQLite file** database by default (`DATABASE_URL=file:/.../dev.db`). Running multiple backend replicas either creates split-brain state (separate DB files/volumes) or requires sharing a single SQLite file across hosts, which is not a reliable deployment pattern. | +| Database | The backend uses **SQLite** by default. For production deployments requiring high availability, use **PostgreSQL** (`DATABASE_PROVIDER=postgresql`). SQLite still works for single-instance deployments but is not recommended for multi-replica setups. | | Collaboration | Real-time presence state is tracked **in-memory** in the backend process, so multiple replicas will fragment presence/collaboration unless a shared Socket.IO adapter is added. | Recommended deployment pattern: @@ -336,14 +336,73 @@ docker compose -f docker-compose.oidc.yml down Base values are documented in `backend/.env.example`. Common ones to care about: -| Variable | Default / Example | Description | -| -------------- | ------------------------- | ----------------------------------------------------------------------------------- | -| `DATABASE_URL` | `file:/app/prisma/dev.db` | SQLite file or external DB URL. | -| `FRONTEND_URL` | `http://localhost:6767` | Allowed frontend origin(s), comma-separated for multiple entries. | -| `TRUST_PROXY` | `false` | `false`, `true`, or hop count (for example `1`). | -| `JWT_SECRET` | `change-this-secret...` | Recommended in production so sessions remain stable across restarts and migrations. | -| `CSRF_SECRET` | `change-this-secret` | Recommended in production so CSRF validation remains stable across restarts. | -| `AUTH_MODE` | `local` | `local`, `hybrid`, `oidc_enforced`. | +| Variable | Default / Example | Description | +| ------------------- | ------------------------- | ----------------------------------------------------------------------------------- | +| `DATABASE_PROVIDER` | `sqlite` | Database provider: `sqlite` or `postgresql`. See [Database Provider](#database-provider) for details. | +| `DATABASE_URL` | `file:/app/prisma/dev.db` | SQLite file or external DB URL. | +| `FRONTEND_URL` | `http://localhost:6767` | Allowed frontend origin(s), comma-separated for multiple entries. | +| `TRUST_PROXY` | `false` | `false`, `true`, or hop count (for example `1`). | +| `JWT_SECRET` | `change-this-secret...` | Recommended in production so sessions remain stable across restarts and migrations. | +| `CSRF_SECRET` | `change-this-secret` | Recommended in production so CSRF validation remains stable across restarts. | +| `AUTH_MODE` | `local` | `local`, `hybrid`, `oidc_enforced`. | + + + +
+Database Provider + +## Database Provider + +ExcaliDash supports both **SQLite** (default) and **PostgreSQL** as database providers. The provider is controlled by the `DATABASE_PROVIDER` environment variable. + +### SQLite (Default) + +SQLite is the default and works out of the box without additional configuration: + +```yaml +# docker-compose.prod.yml +services: + backend: + environment: + - DATABASE_PROVIDER=sqlite + - DATABASE_URL=file:/app/prisma/dev.db +``` + +### PostgreSQL + +To use PostgreSQL instead of SQLite: + +1. **Set the environment variables:** + +```yaml +# docker-compose.prod.yml +services: + backend: + environment: + - DATABASE_PROVIDER=postgresql + - DATABASE_URL=postgresql://user:password@host:5432/excalidash +``` + +2. **Generate PostgreSQL migrations:** + +The repository includes SQLite migrations by default. To use PostgreSQL, you need to generate migrations for the PostgreSQL provider: + +```bash +# 1. Set the provider in your local environment +export DATABASE_PROVIDER=postgresql +export DATABASE_URL="postgresql://user:password@localhost:5432/excalidash" + +# 2. Remove existing SQLite migrations +rm -rf backend/prisma/migrations/postgresql + +# 3. Generate initial migration for PostgreSQL +cd backend +npx prisma migrate dev --name init +``` + +This creates a new `backend/prisma/migrations/postgresql/` folder with PostgreSQL-specific migrations. + +**Note:** When switching database providers, you cannot migrate existing data. The new database will start empty.
diff --git a/backend/.env.example b/backend/.env.example index 455c32ac..ea270d81 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,6 +1,7 @@ # Backend Environment Variables PORT=8000 NODE_ENV=production +# Database provider: sqlite or postgresql DATABASE_PROVIDER=sqlite DATABASE_URL=file:/app/prisma/dev.db FRONTEND_URL=http://localhost:6767 diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh index eec234f6..436d4dfb 100644 --- a/backend/docker-entrypoint.sh +++ b/backend/docker-entrypoint.sh @@ -50,31 +50,33 @@ fi export CSRF_SECRET -# 1. Ensure schema and migrations are present (Running as root) -# Never copy the entire prisma directory, as that can unintentionally overwrite -# persisted SQLite files or copy stray *.db artifacts into the volume. -if [ ! -f "/app/prisma/schema.prisma" ]; then - echo "Mount appears empty (missing schema.prisma). Bootstrapping schema and migrations..." -else - # Volume exists but may be missing new migrations from an upgrade. - echo "Syncing schema and migrations from template..." -fi - -mkdir -p /app/prisma/migrations -cp /app/prisma_template/schema.prisma /app/prisma/schema.prisma -cp -R /app/prisma_template/migrations/. /app/prisma/migrations/ - # Set default DATABASE_PROVIDER if not set if [ -z "${DATABASE_PROVIDER:-}" ]; then echo "DATABASE_PROVIDER not set, defaulting to sqlite" DATABASE_PROVIDER="sqlite" fi +# Validate DATABASE_PROVIDER +if [ "${DATABASE_PROVIDER}" != "sqlite" ] && [ "${DATABASE_PROVIDER}" != "postgresql" ]; then + echo "ERROR: DATABASE_PROVIDER must be 'sqlite' or 'postgresql', got '${DATABASE_PROVIDER}'" + exit 1 +fi + +# Ensure migrations directory exists +mkdir -p /app/prisma/migrations + +# Copy schema.prisma from template +cp /app/prisma_template/schema.prisma /app/prisma/schema.prisma + +# Clear and copy provider-specific migrations folder +echo "Copying ${DATABASE_PROVIDER} migrations..." +rm -rf /app/prisma/migrations/* +cp -R /app/prisma_template/migrations/"${DATABASE_PROVIDER}"/. /app/prisma/migrations/ + # Update schema.prisma with the runtime provider (handles both env() and static values) echo "Configuring Prisma for provider: ${DATABASE_PROVIDER}" sed -i 's/provider = env("[^"]*")/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/schema.prisma sed -i 's/provider = "[^"]*"/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/schema.prisma -sed -i 's/^provider = ".*"/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/migrations/migration_lock.toml # Generate Prisma Client at runtime (run as root since schema is owned by root) echo "Generating Prisma Client..." @@ -86,9 +88,9 @@ cp -r /app/src/generated/* /app/dist/generated/ # 2. Fix permissions unconditionally (Running as root) echo "Fixing filesystem permissions..." -chown -R nodejs:nodejs /app/uploads -chown -R nodejs:nodejs /app/prisma +chown -R nodejs:nodejs /app/uploads /app/prisma /app/dist/generated chmod 755 /app/uploads +chmod -R 755 /app/dist/generated chmod 600 "${JWT_SECRET_FILE}" chmod 600 "${CSRF_SECRET_FILE}" diff --git a/backend/package-lock.json b/backend/package-lock.json index ddaa75c7..1b579b58 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "backend", - "version": "0.4.7", + "version": "0.4.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "backend", - "version": "0.4.7", + "version": "0.4.27", "license": "ISC", "dependencies": { "@prisma/client": "^5.22.0", @@ -1177,6 +1177,7 @@ "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" } @@ -2676,6 +2677,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.0.tgz", "integrity": "sha512-XdpJDLxfztVY59X0zPI6sibRiGcxhTPXRD3IhJmjKf2jwMvkRGV1j7loB8U+heeamoU3XvihAaGRTR4aXXUN3A==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -4164,6 +4166,7 @@ "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/engines": "5.22.0" }, @@ -5176,6 +5179,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5334,6 +5338,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5424,6 +5429,7 @@ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -5517,6 +5523,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/backend/prisma.config.ts b/backend/prisma.config.ts deleted file mode 100644 index d73df7b3..00000000 --- a/backend/prisma.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import "dotenv/config"; -import { defineConfig, env } from "prisma/config"; - -export default defineConfig({ - schema: "prisma/schema.prisma", - migrations: { - path: "prisma/migrations", - }, - datasource: { - url: env("DATABASE_URL"), - }, -}); diff --git a/backend/prisma/migrations/postgresql/20260225171236_init/migration.sql b/backend/prisma/migrations/postgresql/20260225171236_init/migration.sql new file mode 100644 index 00000000..6f0327d2 --- /dev/null +++ b/backend/prisma/migrations/postgresql/20260225171236_init/migration.sql @@ -0,0 +1,233 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "username" TEXT, + "email" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "name" TEXT NOT NULL, + "role" TEXT NOT NULL DEFAULT 'USER', + "mustResetPassword" BOOLEAN NOT NULL DEFAULT false, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SystemConfig" ( + "id" TEXT NOT NULL DEFAULT 'default', + "authEnabled" BOOLEAN NOT NULL DEFAULT false, + "authOnboardingCompleted" BOOLEAN NOT NULL DEFAULT false, + "registrationEnabled" BOOLEAN NOT NULL DEFAULT false, + "authLoginRateLimitEnabled" BOOLEAN NOT NULL DEFAULT true, + "authLoginRateLimitWindowMs" INTEGER NOT NULL DEFAULT 900000, + "authLoginRateLimitMax" INTEGER NOT NULL DEFAULT 20, + "bootstrapSetupCodeHash" TEXT, + "bootstrapSetupCodeIssuedAt" TIMESTAMP(3), + "bootstrapSetupCodeExpiresAt" TIMESTAMP(3), + "bootstrapSetupCodeFailedAttempts" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "SystemConfig_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Collection" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Collection_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Drawing" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "elements" TEXT NOT NULL, + "appState" TEXT NOT NULL, + "files" TEXT NOT NULL DEFAULT '{}', + "preview" TEXT, + "version" INTEGER NOT NULL DEFAULT 1, + "userId" TEXT NOT NULL, + "collectionId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Drawing_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DrawingPermission" ( + "id" TEXT NOT NULL, + "drawingId" TEXT NOT NULL, + "granteeUserId" TEXT NOT NULL, + "permission" TEXT NOT NULL, + "createdByUserId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "DrawingPermission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DrawingLinkShare" ( + "id" TEXT NOT NULL, + "drawingId" TEXT NOT NULL, + "permission" TEXT NOT NULL, + "tokenHash" TEXT NOT NULL, + "passphraseHash" TEXT, + "expiresAt" TIMESTAMP(3), + "revokedAt" TIMESTAMP(3), + "failedAttempts" INTEGER NOT NULL DEFAULT 0, + "lockedUntil" TIMESTAMP(3), + "lastUsedAt" TIMESTAMP(3), + "lastUsedIp" TEXT, + "createdByUserId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "DrawingLinkShare_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Library" ( + "id" TEXT NOT NULL, + "items" TEXT NOT NULL DEFAULT '[]', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Library_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PasswordResetToken" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "used" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RefreshToken" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "revoked" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "RefreshToken_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AuditLog" ( + "id" TEXT NOT NULL, + "userId" TEXT, + "action" TEXT NOT NULL, + "resource" TEXT, + "ipAddress" TEXT, + "userAgent" TEXT, + "details" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AuthIdentity" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "issuer" TEXT NOT NULL, + "subject" TEXT NOT NULL, + "emailAtLink" TEXT NOT NULL, + "lastLoginAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "AuthIdentity_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE INDEX "Collection_userId_updatedAt_idx" ON "Collection"("userId", "updatedAt"); + +-- CreateIndex +CREATE INDEX "Drawing_userId_updatedAt_idx" ON "Drawing"("userId", "updatedAt"); + +-- CreateIndex +CREATE INDEX "Drawing_userId_collectionId_updatedAt_idx" ON "Drawing"("userId", "collectionId", "updatedAt"); + +-- CreateIndex +CREATE INDEX "DrawingPermission_granteeUserId_idx" ON "DrawingPermission"("granteeUserId"); + +-- CreateIndex +CREATE INDEX "DrawingPermission_drawingId_idx" ON "DrawingPermission"("drawingId"); + +-- CreateIndex +CREATE UNIQUE INDEX "DrawingPermission_drawingId_granteeUserId_key" ON "DrawingPermission"("drawingId", "granteeUserId"); + +-- CreateIndex +CREATE UNIQUE INDEX "DrawingLinkShare_tokenHash_key" ON "DrawingLinkShare"("tokenHash"); + +-- CreateIndex +CREATE INDEX "DrawingLinkShare_drawingId_idx" ON "DrawingLinkShare"("drawingId"); + +-- CreateIndex +CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "RefreshToken_token_key" ON "RefreshToken"("token"); + +-- CreateIndex +CREATE INDEX "AuthIdentity_userId_idx" ON "AuthIdentity"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "AuthIdentity_issuer_subject_key" ON "AuthIdentity"("issuer", "subject"); + +-- CreateIndex +CREATE UNIQUE INDEX "AuthIdentity_provider_userId_key" ON "AuthIdentity"("provider", "userId"); + +-- AddForeignKey +ALTER TABLE "Collection" ADD CONSTRAINT "Collection_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Drawing" ADD CONSTRAINT "Drawing_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Drawing" ADD CONSTRAINT "Drawing_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DrawingPermission" ADD CONSTRAINT "DrawingPermission_drawingId_fkey" FOREIGN KEY ("drawingId") REFERENCES "Drawing"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DrawingPermission" ADD CONSTRAINT "DrawingPermission_granteeUserId_fkey" FOREIGN KEY ("granteeUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DrawingLinkShare" ADD CONSTRAINT "DrawingLinkShare_drawingId_fkey" FOREIGN KEY ("drawingId") REFERENCES "Drawing"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RefreshToken" ADD CONSTRAINT "RefreshToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AuthIdentity" ADD CONSTRAINT "AuthIdentity_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/postgresql/migration_lock.toml b/backend/prisma/migrations/postgresql/migration_lock.toml new file mode 100644 index 00000000..fbffa92c --- /dev/null +++ b/backend/prisma/migrations/postgresql/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/backend/prisma/migrations/20251122021659_init/migration.sql b/backend/prisma/migrations/sqlite/20251122021659_init/migration.sql similarity index 100% rename from backend/prisma/migrations/20251122021659_init/migration.sql rename to backend/prisma/migrations/sqlite/20251122021659_init/migration.sql diff --git a/backend/prisma/migrations/20251122032308_add_preview/migration.sql b/backend/prisma/migrations/sqlite/20251122032308_add_preview/migration.sql similarity index 100% rename from backend/prisma/migrations/20251122032308_add_preview/migration.sql rename to backend/prisma/migrations/sqlite/20251122032308_add_preview/migration.sql diff --git a/backend/prisma/migrations/20251122065455_add_files_column/migration.sql b/backend/prisma/migrations/sqlite/20251122065455_add_files_column/migration.sql similarity index 100% rename from backend/prisma/migrations/20251122065455_add_files_column/migration.sql rename to backend/prisma/migrations/sqlite/20251122065455_add_files_column/migration.sql diff --git a/backend/prisma/migrations/20251124220546_add_library_model/migration.sql b/backend/prisma/migrations/sqlite/20251124220546_add_library_model/migration.sql similarity index 100% rename from backend/prisma/migrations/20251124220546_add_library_model/migration.sql rename to backend/prisma/migrations/sqlite/20251124220546_add_library_model/migration.sql diff --git a/backend/prisma/migrations/20260124145151_add_user_auth/migration.sql b/backend/prisma/migrations/sqlite/20260124145151_add_user_auth/migration.sql similarity index 100% rename from backend/prisma/migrations/20260124145151_add_user_auth/migration.sql rename to backend/prisma/migrations/sqlite/20260124145151_add_user_auth/migration.sql diff --git a/backend/prisma/migrations/20260124152839_add_password_reset_audit_refresh_tokens/migration.sql b/backend/prisma/migrations/sqlite/20260124152839_add_password_reset_audit_refresh_tokens/migration.sql similarity index 100% rename from backend/prisma/migrations/20260124152839_add_password_reset_audit_refresh_tokens/migration.sql rename to backend/prisma/migrations/sqlite/20260124152839_add_password_reset_audit_refresh_tokens/migration.sql diff --git a/backend/prisma/migrations/20260206173000_add_auth_enabled_system_config/migration.sql b/backend/prisma/migrations/sqlite/20260206173000_add_auth_enabled_system_config/migration.sql similarity index 100% rename from backend/prisma/migrations/20260206173000_add_auth_enabled_system_config/migration.sql rename to backend/prisma/migrations/sqlite/20260206173000_add_auth_enabled_system_config/migration.sql diff --git a/backend/prisma/migrations/20260206195000_add_auth_login_rate_limit_config/migration.sql b/backend/prisma/migrations/sqlite/20260206195000_add_auth_login_rate_limit_config/migration.sql similarity index 100% rename from backend/prisma/migrations/20260206195000_add_auth_login_rate_limit_config/migration.sql rename to backend/prisma/migrations/sqlite/20260206195000_add_auth_login_rate_limit_config/migration.sql diff --git a/backend/prisma/migrations/20260207000000_add_query_indexes/migration.sql b/backend/prisma/migrations/sqlite/20260207000000_add_query_indexes/migration.sql similarity index 100% rename from backend/prisma/migrations/20260207000000_add_query_indexes/migration.sql rename to backend/prisma/migrations/sqlite/20260207000000_add_query_indexes/migration.sql diff --git a/backend/prisma/migrations/20260210153000_add_auth_onboarding_completed/migration.sql b/backend/prisma/migrations/sqlite/20260210153000_add_auth_onboarding_completed/migration.sql similarity index 100% rename from backend/prisma/migrations/20260210153000_add_auth_onboarding_completed/migration.sql rename to backend/prisma/migrations/sqlite/20260210153000_add_auth_onboarding_completed/migration.sql diff --git a/backend/prisma/migrations/20260210190000_add_auth_identity/migration.sql b/backend/prisma/migrations/sqlite/20260210190000_add_auth_identity/migration.sql similarity index 100% rename from backend/prisma/migrations/20260210190000_add_auth_identity/migration.sql rename to backend/prisma/migrations/sqlite/20260210190000_add_auth_identity/migration.sql diff --git a/backend/prisma/migrations/20260211232000_add_bootstrap_setup_code/migration.sql b/backend/prisma/migrations/sqlite/20260211232000_add_bootstrap_setup_code/migration.sql similarity index 100% rename from backend/prisma/migrations/20260211232000_add_bootstrap_setup_code/migration.sql rename to backend/prisma/migrations/sqlite/20260211232000_add_bootstrap_setup_code/migration.sql diff --git a/backend/prisma/migrations/20260217214759_drawing_sharing/migration.sql b/backend/prisma/migrations/sqlite/20260217214759_drawing_sharing/migration.sql similarity index 100% rename from backend/prisma/migrations/20260217214759_drawing_sharing/migration.sql rename to backend/prisma/migrations/sqlite/20260217214759_drawing_sharing/migration.sql diff --git a/backend/prisma/migrations/migration_lock.toml b/backend/prisma/migrations/sqlite/migration_lock.toml similarity index 100% rename from backend/prisma/migrations/migration_lock.toml rename to backend/prisma/migrations/sqlite/migration_lock.toml diff --git a/docker-compose.pg-test.yml b/docker-compose.pg-test.yml new file mode 100644 index 00000000..3cb9e9c1 --- /dev/null +++ b/docker-compose.pg-test.yml @@ -0,0 +1,43 @@ +services: + # PostgreSQL database for testing + postgres: + image: postgres:16-alpine + environment: + - POSTGRES_USER=excalidash + - POSTGRES_PASSWORD=test123 + - POSTGRES_DB=excalidash + ports: + - "5433:5432" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U excalidash"] + interval: 5s + timeout: 5s + retries: 5 + restart: unless-stopped + + # Backend with PostgreSQL + backend: + build: + context: ./backend + dockerfile: Dockerfile + environment: + - DATABASE_PROVIDER=postgresql + - DATABASE_URL=postgresql://excalidash:test123@postgres:5432/excalidash + - FRONTEND_URL=http://localhost:6767 + - JWT_SECRET=test-secret-pg-min-32-chars-long + - CSRF_SECRET=test-secret-pg + - AUTH_MODE=local + ports: + - "8000:8000" + depends_on: + postgres: + condition: service_healthy + volumes: + - uploads:/app/uploads + restart: unless-stopped + +volumes: + pgdata: + uploads: diff --git a/scripts/generate-migrations.sh b/scripts/generate-migrations.sh new file mode 100755 index 00000000..d871624a --- /dev/null +++ b/scripts/generate-migrations.sh @@ -0,0 +1,147 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +PRISMA_DIR="${PROJECT_DIR}/backend/prisma" +SCHEMA_FILE="${PRISMA_DIR}/schema.prisma" +MIGRATIONS_DIR="${PRISMA_DIR}/migrations" + +echo "=== ExcaliDash Migration Generator ===" +echo "" +echo "Select database provider:" +echo "1) SQLite" +echo "2) PostgreSQL" +echo "" +read -p "Enter choice (1 or 2): " choice + +case "$choice" in + 1) + PROVIDER="sqlite" + ;; + 2) + PROVIDER="postgresql" + ;; + *) + echo "Invalid choice. Exiting." + exit 1 + ;; +esac + +echo "" +echo "Provider selected: ${PROVIDER}" + +OTHER_PROVIDER=$([ "$PROVIDER" = "sqlite" ] && echo "postgresql" || echo "sqlite") + +# Check if provider folder exists +if [ ! -d "${MIGRATIONS_DIR}/${PROVIDER}" ]; then + echo "ERROR: Migrations folder for '${PROVIDER}' does not exist at ${MIGRATIONS_DIR}/${PROVIDER}" + exit 1 +fi + +echo "" +echo "Step 1: Backing up schema.prisma..." + +# Backup current schema.prisma +cp "${SCHEMA_FILE}" "${SCHEMA_FILE}.backup" + +echo "" +echo "Step 2: Updating schema.prisma to use ${PROVIDER}..." + +# Update schema.prisma to use the selected provider +sed -i 's/provider = env("DATABASE_PROVIDER")/provider = "'"${PROVIDER}"'"/' "${SCHEMA_FILE}" + +echo "" +echo "Step 3: Setting up migrations..." + +# Clear migrations folder +rm -rf "${MIGRATIONS_DIR}"/* + +# Recreate folders +mkdir -p "${MIGRATIONS_DIR}/sqlite" +mkdir -p "${MIGRATIONS_DIR}/postgresql" + +# Copy migrations for selected provider (if any exist) +if [ "$PROVIDER" = "sqlite" ]; then + # SQLite has existing migrations - copy them + cp -R "${MIGRATIONS_DIR}/sqlite/." "${MIGRATIONS_DIR}/" + echo " Copied $(ls -1 ${MIGRATIONS_DIR} | wc -l) SQLite migrations" +else + # PostgreSQL - create from empty database + echo " Will create initial PostgreSQL migration from schema" +fi + +echo "" +echo "Step 4: Running Prisma migrate..." +echo "" + +# Check for DATABASE_URL +if [ -z "${DATABASE_URL}" ]; then + echo "DATABASE_URL is not set. Please enter your database URL:" + echo "Examples:" + echo " SQLite: file:./dev.db" + echo " PostgreSQL: postgresql://user:password@localhost:5432/excalidash" + read -p "DATABASE_URL: " DATABASE_URL + export DATABASE_URL +fi + +cd "${PROJECT_DIR}/backend" + +# Run prisma migrate +if [ "$1" = "--dev" ]; then + MIGRATION_NAME="${2:-new_migration}" + echo " Running: npx prisma migrate dev --name ${MIGRATION_NAME}" + npx prisma migrate dev --name "${MIGRATION_NAME}" +else + echo " Running: npx prisma migrate deploy" + npx prisma migrate deploy +fi + +echo "" +echo "Step 5: Restoring schema.prisma..." + +# Restore original schema.prisma +mv "${SCHEMA_FILE}.backup" "${SCHEMA_FILE}" + +echo "" +echo "Step 6: Organizing migrations..." + +# Clear migrations folder +rm -rf "${MIGRATIONS_DIR}"/* + +# Recreate folders +mkdir -p "${MIGRATIONS_DIR}/sqlite" +mkdir -p "${MIGRATIONS_DIR}/postgresql" + +# Restore other provider (empty for postgresql, or from git for sqlite) +if [ "$PROVIDER" = "postgresql" ]; then + # PostgreSQL folder - just the lock file + echo 'provider = "postgresql"' > "${MIGRATIONS_DIR}/postgresql/migration_lock.toml" + # Restore SQLite from git + git checkout HEAD -- prisma/migrations/ 2>/dev/null || true + mkdir -p sqlite postgresql 2>/dev/null + mv 202* sqlite/ 2>/dev/null || true + mv migration_lock.toml sqlite/ 2>/dev/null || true + echo 'provider = "postgresql"' > postgresql/migration_lock.toml 2>/dev/null || true +else + # SQLite - restore PostgreSQL (empty) + echo 'provider = "postgresql"' > "${MIGRATIONS_DIR}/postgresql/migration_lock.toml" +fi + +# Move newly generated migrations to the provider folder +if [ "$(ls -d ${MIGRATIONS_DIR}/2* 2>/dev/null)" ]; then + for dir in "${MIGRATIONS_DIR}"/2*/; do + if [ -d "$dir" ]; then + cp -R "$dir" "${MIGRATIONS_DIR}/${PROVIDER}/" + rm -rf "$dir" + fi + done +fi + +echo "" +echo "=== Done! ===" +echo "" +echo "Generated migrations are in: ${MIGRATIONS_DIR}/${PROVIDER}/" +ls -la "${MIGRATIONS_DIR}/${PROVIDER}/" +echo "" +echo "Your schema.prisma has been restored to use env(DATABASE_PROVIDER)" From ad41a7b883fdda799cc02468f16f5ce7543142ff Mon Sep 17 00:00:00 2001 From: BaptGosse Date: Mon, 9 Mar 2026 01:07:35 +0100 Subject: [PATCH 6/6] fix: health check, because kubernetes do it in http --- backend/src/__tests__/health-check.test.ts | 58 ++++++++++++++++++++++ backend/src/index.ts | 5 ++ 2 files changed, 63 insertions(+) create mode 100644 backend/src/__tests__/health-check.test.ts diff --git a/backend/src/__tests__/health-check.test.ts b/backend/src/__tests__/health-check.test.ts new file mode 100644 index 00000000..8d943908 --- /dev/null +++ b/backend/src/__tests__/health-check.test.ts @@ -0,0 +1,58 @@ +/** + * Health check endpoint test + * Ensures /health returns 200 OK without redirects (for Kubernetes probes) + */ + +import { describe, it, expect, beforeEach } from "vitest"; +import express from "express"; +import request from "supertest"; + +describe("Health check endpoint", () => { + it("should return 200 OK with status 'ok'", async () => { + const app = express(); + + app.get("/health", (req, res) => { + res.status(200).json({ status: "ok" }); + }); + + const response = await request(app).get("/health"); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ status: "ok" }); + }); + + it("should not redirect even when behind HTTPS middleware", async () => { + const app = express(); + + // Simulate HTTPS redirect middleware with /health exception + app.use((req, res, next) => { + // Skip HTTPS redirect for health check endpoint + if (req.path === "/health") { + return next(); + } + + if (req.header("x-forwarded-proto") !== "https") { + return res.redirect(`https://example.com${req.path}`); + } + next(); + }); + + app.get("/health", (req, res) => { + res.status(200).json({ status: "ok" }); + }); + + app.get("/other", (req, res) => { + res.status(200).json({ status: "ok" }); + }); + + // Health check should NOT redirect (even without x-forwarded-proto) + const healthResponse = await request(app).get("/health"); + expect(healthResponse.status).toBe(200); + expect(healthResponse.body).toEqual({ status: "ok" }); + + // Other routes should still redirect + const otherResponse = await request(app).get("/other"); + expect(otherResponse.status).toBe(302); + expect(otherResponse.headers.location).toContain("https://"); + }); +}); diff --git a/backend/src/index.ts b/backend/src/index.ts index f0c1e29a..b34f7fbb 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -243,6 +243,11 @@ if (shouldEnforceHttps) { ); app.use((req, res, next) => { + // Skip HTTPS redirect for health check endpoint (for Kubernetes probes) + if (req.path === "/health") { + return next(); + } + if (req.header("x-forwarded-proto") !== "https") { // Avoid Host-header based open redirects; prefer a configured canonical origin/host. const rawHost = String(req.header("host") || "").trim().toLowerCase();