Skip to content

Backend review: security, correctness and performance fixes - #443

Merged
pashidlos merged 8 commits into
masterfrom
claude/project-review-optimization-623b8f
Nov 1, 2026
Merged

pashidlos merged 8 commits into
masterfrom
claude/project-review-optimization-623b8f

Conversation

@pashidlos

@pashidlos pashidlos commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

A full review of the backend with its fixes: security, correctness, performance, indexes, a leaner test suite and a stricter build. Backward compatible with existing SDKs, the frontend and deployments: an upgrade needs no manual step.

Worth knowing when upgrading

  • JWT_SECRET: a publicly known value (jwtPrivateKey, the one in .env) only logs a warning; set a random one (openssl rand -hex 32).
  • Migrations: 4 new ones. Three add indexes with CREATE INDEX CONCURRENTLY, one adds TestRun.statusBeforeDecision. The migration service applies them. Writes are not blocked, but avoid deploying around 00:00, when the nightly cleanup runs.
  • Non-root container: the API runs as uid 1000 when the /imageUploads volume and the TLS files are accessible to it. Otherwise it stays root and logs a warning. To switch, run chown -R 1000:1000 <images volume> (and the TLS files).
  • HTTPS_KEY_PATH / HTTPS_CERT_PATH are now read: if a deployment set them and the files exist, the server starts serving HTTPS.
  • Redis: if Redis is unreachable at startup, the API exits with code 1 after 30 s instead of waiting forever.
  • Seed:
    • no longer logs users, hashes or keys;
    • exits non-zero on error;
    • never promotes an existing user to admin;
    • when env does not set them, generates the first admin's password and key and prints them once.
  • JWT: tokens carry sub (the user id). Older email-only tokens are still accepted.
  • Compose: Postgres listens on 127.0.0.1 only; a tool that reached it from another host needs its own port mapping. api starts after the migration succeeds and has a healthcheck.

API changes (backward compatible)

  • Error body is {statusCode, path, name, message}, without exception or stack.

  • Client errors that returned 500 now return 4xx:

    • non-PNG upload: 400;
    • oversized JSON body: 413;
    • PNG over MAX_IMAGE_PIXELS: 422;
    • P2003: 409;
    • run without a variation: 409;
    • unknown project on delete: 404;
    • negative take/skip: 400.

    A full diff queue answers 503 with Retry-After. /health answers 503 without a database.

  • New endpoints: POST /test-runs/revert (undo an approve or reject) and GET /users/registration/enabled.

  • apiKey removed from /users/all and PATCH /users/assignRole responses.

  • PATCH /test-runs/update/:id changes only comment.

  • Global ValidationPipe with whitelist: unknown body fields are dropped silently, not rejected.

  • Websocket testRun_* events:

    • they carry TestRunDto (no changeSignature, pixelMisMatchCount, createdAt, updatedAt, projectId);
    • empty batches are not sent;
    • build_created goes out once;
    • every update goes out as testRun_updated, and also refreshes a still-queued testRun_created.
  • Image names are validated (closes an open redirect). Downloads are streamed with Cache-Control: immutable.

  • LDAP:

    • an empty password is rejected;
    • a directory outage gives 503;
    • changePassword gives 400;
    • PUT /users ignores the email in the body.

Behaviour

  • Ignore areas are rounded outwards and clipped to the image. Integer areas give byte-identical results. Fractional ones (Playwright) now actually mask pixels. Huge negative ones no longer hang the worker.
  • Auto-approve reaches the same verdicts faster: each baseline is compared once, and size mismatches are rejected from the PNG header. VLM projects use the pixelmatch verdict there.
  • Build retention skips builds with uploads in the last 60 minutes and runs in the background. Manual delete is unchanged.
  • Repeated approve no longer duplicates the baseline.
  • Deletes remove rows before files and no longer swallow errors.
  • A foreign buildId on upload is accepted with a warning.
  • An odiff project on S3 cannot be re-saved until its comparator changes (it never worked).

New environment variables

All optional, with defaults: REGISTRATION_ENABLED (true), MAX_IMAGE_PIXELS (100 MP, 0 = no limit), DIFF_WORKER_MAX_HEAP_MB (512), MULTIPART_FILE_SIZE_LIMIT (100 MiB), LOG_LEVEL. Timeouts for Ollama and Gemini (5 min), S3 (5 s connect, 60 s request), LDAP (5 s connect, 10 s operation), Redis (30 s) and a diff job (120 s) are fixed in code, as are VLM concurrency (4) and the retention grace (60 min). Details are in the README, under "Environment variables".

Development

  • Tests:
    • unit suites run on @swc/jest, and ts-jest is removed;
    • coverage thresholds raised to 98.5 / 98 / 99 / 98.5;
    • new e2e suite on real worker threads.
  • Lint: type-aware promise-safety rules, plus noUnusedLocals, noImplicitReturns, noImplicitOverride and noFallthroughCasesInSwitch.
  • Dependencies:
    • removed lodash, body-parser, passport-local, tsconfig-paths, eslint-config-prettier and the root jest-mock-extended;
    • express and socket.io are declared explicitly.
  • CI:
    • actions pinned by SHA;
    • a check that migrations match the schema;
    • integration images are built once, with a cache.
    • Docker images are built and published only from release tags; on pull requests CI builds and runs the api and migration images itself.
  • Docker:
    • the image build is gated on the type check;
    • NODE_TLS_REJECT_UNAUTHORIZED=0 removed from both Dockerfiles.

How it was verified

On the head commit:

  • npm run typecheck, npm run lint, npm run format:check: clean.
  • npm run test:cov: 1629 tests, coverage 99.94 / 99.15 / 100 / 99.94.
  • npm run build: passes.
  • On a fresh Postgres 15:
    • npx prisma migrate deploy applies every migration;
    • prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --exit-code reports no difference;
    • npm run test:e2e: 302 tests.
  • The frontend's Playwright smoke against the stack: 15/15 over 5 repeats (on b47b130).
  • The compose stack (/health, test:acceptance, test:ldap, test:oidc) runs in this PR's CI; the prisma/ package tests (23) passed earlier in the review.

Left as is

  • The DINOv2 patch gate is still computed when a project has dinov2Check on but the server has no DINOV2_URL. The run then goes to review with a warning in the log, so nothing is approved by mistake; only the gate computation is wasted.
  • src/compare/test-utils.ts holds PNG helpers shared by the compare specs, so it is excluded from the build and from coverage.

Follow-ups

  • Next API stage with the frontend:
    • POST /test-variations/merge;
    • websocket JWT behind WS_AUTH_REQUIRED, plus join/leave rooms;
    • masked Gemini key;
    • per-id results from bulk endpoints.
  • A committed openapi.json with a drift check in CI.
  • A multer 2.5.0 override, once the release is two weeks old.

🤖 Generated with Claude Code

Security
- Refuse to start with the default JWT_SECRET under NODE_ENV=production.
- JWT carries `sub`; email-only tokens accepted while JWT_LEGACY_EMAIL_TOKENS
  is not false.
- Global ValidationPipe with whitelist: unknown body fields are dropped.
- PATCH /test-runs/update/:id changes only `comment`.
- /users/all and PATCH /users/assignRole no longer return apiKey.
- Image names validated (closes an open redirect); downloads are streamed.
- Seed no longer logs users, never promotes an existing user, generates a
  first admin's password and key when env does not set them.
- Container runs as uid 1000 when the image volume allows it
  (VRT_RUN_AS_ROOT to keep root); HTTPS_KEY_PATH / HTTPS_CERT_PATH are read.
- REGISTRATION_ENABLED, STRICT_BUILD_PROJECT_CHECK; LDAP rejects empty
  passwords and times out.

API (backward compatible)
- Error body is {statusCode, path, name, message}; client errors that gave
  500 now give 400/404/409/413/422; a full diff queue gives 503 with
  Retry-After; /health gives 503 without a database.
- New: POST /test-runs/revert, GET /users/registration/enabled.
- Websocket testRun_* events carry TestRunDto; empty events are not sent.

Behaviour and performance
- Ignore areas are rounded and clipped to the image.
- Auto-approve compares each baseline once and skips size mismatches by the
  PNG header; VLM projects use the pixelmatch verdict there.
- Build retention skips builds with recent uploads
  (BUILD_RETENTION_GRACE_MINUTES) and runs in the background.
- Repeated approve no longer duplicates the baseline; deletes remove rows
  before files and surface errors.
- Image work stays in the diff worker pool with job timeouts and heap limits;
  timeouts for S3, LDAP, Redis, Ollama and Gemini.

Database
- Three indexes via CREATE INDEX CONCURRENTLY and TestRun.statusBeforeDecision.

Tests and build
- Coverage thresholds raised and met; e2e and auth setup deduplicated.
- Docker image gated on typecheck; compose binds Postgres to 127.0.0.1,
  api waits for the migration and has a healthcheck.
Clients skip testRun_created for runs they already fetched over HTTP, so an
approve within the debounce window left the UI on the old status. The queued
created entry is still refreshed, and the run now goes to the updated queue
too, so both events carry the latest state.
@coderabbitai

coderabbitai Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 240 files, which is 140 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 3339d56 and ec45224.

Files ignored due to path filters: 2
  • package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • prisma/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
Files selected for processing: 240
  • .claude/skills/pre-pr-review/SKILL.md
  • .env
  • .github/dependabot.yml
  • .github/workflows/docker.yml
  • .github/workflows/workflow.yml
  • .gitignore
  • AGENTS.md
  • Dockerfile
  • README.md
  • docker-compose.ldap.yml
  • docker-compose.oidc.yml
  • docker-compose.yml
  • docker-entrypoint.sh
  • eslint.config.mjs
  • jest.config.ts
  • package.json
  • prisma/Dockerfile
  • prisma/client.ts
  • prisma/migrations/20261024174953_add_test_run_test_variation_index/migration.sql
  • prisma/migrations/20261024175021_add_baseline_test_variation_index/migration.sql
  • prisma/migrations/20261024175038_add_test_run_name_baseline_name_index/migration.sql
  • prisma/migrations/20261101165840_add_test_run_status_before_decision/migration.sql
  • prisma/package.json
  • prisma/pg-options.ts
  • prisma/schema.prisma
  • prisma/seed.ts
  • prisma/test/seed.spec.ts
  • src/_data_/index.ts
  • src/app.module.spec.ts
  • src/app.module.ts
  • src/app.service.spec.ts
  • src/app.service.ts
  • src/auth/auth.service.spec.ts
  • src/auth/auth.service.ts
  • src/auth/guards/api.guard.spec.ts
  • src/auth/guards/api.guard.ts
  • src/auth/guards/mixed.guard.spec.ts
  • src/auth/guards/mixed.guard.ts
  • src/auth/guards/role.guard.spec.ts
  • src/auth/guards/role.guard.ts
  • src/auth/jwt-payload.model.ts
  • src/auth/jwt.strategy.spec.ts
  • src/auth/jwt.strategy.ts
  • src/builds/build-stats.spec.ts
  • src/builds/build-stats.ts
  • src/builds/builds.controller.spec.ts
  • src/builds/builds.controller.ts
  • src/builds/builds.module.ts
  • src/builds/builds.service.spec.ts
  • src/builds/builds.service.ts
  • src/builds/dto/build-modify.dto.ts
  • src/builds/dto/build.dto.spec.ts
  • src/builds/dto/build.dto.ts
  • src/compare/compare.service.spec.ts
  • src/compare/compare.service.ts
  • src/compare/diff-worker-pool.inline.spec.ts
  • src/compare/diff-worker-pool.spec.ts
  • src/compare/diff-worker-pool.ts
  • src/compare/diff-worker-pool.workers.spec.ts
  • src/compare/libs/ImageCompareInput.ts
  • src/compare/libs/blur-amplitude.config.spec.ts
  • src/compare/libs/blur-amplitude.config.ts
  • src/compare/libs/dinov2/dinov2.controller.spec.ts
  • src/compare/libs/dinov2/dinov2.controller.ts
  • src/compare/libs/dinov2/dinov2.service.spec.ts
  • src/compare/libs/dinov2/dinov2.service.ts
  • src/compare/libs/image-comparator.interface.ts
  • src/compare/libs/looks-same/looks-same.core.spec.ts
  • src/compare/libs/looks-same/looks-same.core.ts
  • src/compare/libs/looks-same/looks-same.service.boundaries.spec.ts
  • src/compare/libs/looks-same/looks-same.service.real-images.spec.ts
  • src/compare/libs/looks-same/looks-same.service.spec.ts
  • src/compare/libs/looks-same/looks-same.service.ts
  • src/compare/libs/looks-same/looks-same.types.ts
  • src/compare/libs/odiff/odiff.service.spec.ts
  • src/compare/libs/odiff/odiff.service.ts
  • src/compare/libs/odiff/odiff.types.ts
  • src/compare/libs/pixelmatch/blur-amplitude.core.spec.ts
  • src/compare/libs/pixelmatch/blur-amplitude.core.ts
  • src/compare/libs/pixelmatch/patch-gate.core.spec.ts
  • src/compare/libs/pixelmatch/patch-gate.core.ts
  • src/compare/libs/pixelmatch/pixelmatch.core.real-images.spec.ts
  • src/compare/libs/pixelmatch/pixelmatch.core.spec.ts
  • src/compare/libs/pixelmatch/pixelmatch.core.ts
  • src/compare/libs/pixelmatch/pixelmatch.service.real-images.spec.ts
  • src/compare/libs/pixelmatch/pixelmatch.service.spec.ts
  • src/compare/libs/pixelmatch/pixelmatch.service.ts
  • src/compare/libs/pixelmatch/pixelmatch.worker.spec.ts
  • src/compare/libs/pixelmatch/pixelmatch.worker.ts
  • src/compare/libs/pixelmatch/signature.core.spec.ts
  • src/compare/libs/pixelmatch/signature.core.ts
  • src/compare/libs/pixelmatch/thumbnail.core.spec.ts
  • src/compare/libs/pixelmatch/thumbnail.core.ts
  • src/compare/libs/pixelmatch/worker-job.spec.ts
  • src/compare/libs/pixelmatch/worker-job.ts
  • src/compare/libs/vlm/concurrency-limiter.spec.ts
  • src/compare/libs/vlm/concurrency-limiter.ts
  • src/compare/libs/vlm/providers/gemini/gemini.service.spec.ts
  • src/compare/libs/vlm/providers/gemini/gemini.service.ts
  • src/compare/libs/vlm/providers/ollama/ollama.controller.spec.ts
  • src/compare/libs/vlm/providers/ollama/ollama.service.spec.ts
  • src/compare/libs/vlm/providers/ollama/ollama.service.ts
  • src/compare/libs/vlm/vlm-inputs.core.spec.ts
  • src/compare/libs/vlm/vlm-inputs.core.ts
  • src/compare/libs/vlm/vlm-provider.interface.ts
  • src/compare/libs/vlm/vlm-result.schema.ts
  • src/compare/libs/vlm/vlm.service.spec.ts
  • src/compare/libs/vlm/vlm.service.ts
  • src/compare/libs/vlm/vlm.types.ts
  • src/compare/test-utils.ts
  • src/compare/utils/index.spec.ts
  • src/compare/utils/index.ts
  • src/config/env.validation.spec.ts
  • src/config/env.validation.ts
  • src/health/health.controller.spec.ts
  • src/health/health.controller.ts
  • src/http-exception.filter.spec.ts
  • src/http-exception.filter.ts
  • src/jest.config.ts
  • src/jest.setup.ts
  • src/main.helpers.spec.ts
  • src/main.helpers.ts
  • src/main.ts
  • src/prisma/prisma-errors.spec.ts
  • src/prisma/prisma-errors.ts
  • src/prisma/prisma.module.spec.ts
  • src/prisma/prisma.module.ts
  • src/prisma/prisma.service.spec.ts
  • src/prisma/prisma.service.ts
  • src/projects/dto/project.dto.spec.ts
  • src/projects/dto/project.dto.ts
  • src/projects/projects.controller.spec.ts
  • src/projects/projects.controller.ts
  • src/projects/projects.module.ts
  • src/projects/projects.service.spec.ts
  • src/projects/projects.service.ts
  • src/redis-io.adapter.spec.ts
  • src/redis-io.adapter.ts
  • src/shared/api-file.decorator.spec.ts
  • src/shared/current-user.decorator.spec.ts
  • src/shared/dto/baseline.dto.ts
  • src/shared/events/debounce.spec.ts
  • src/shared/events/debounce.ts
  • src/shared/events/events.gateway.spec.ts
  • src/shared/events/events.gateway.ts
  • src/shared/fite-to-body.interceptor.spec.ts
  • src/shared/tasks/tasks.service.spec.ts
  • src/shared/tasks/tasks.service.ts
  • src/shared/validation.audit.spec.ts
  • src/shared/validation.spec.ts
  • src/shared/validation.ts
  • src/shared/validation.whitelist.spec.ts
  • src/shared/wrapper-type.ts
  • src/static/aws/s3.service.spec.ts
  • src/static/aws/s3.service.ts
  • src/static/hdd/hdd.service.spec.ts
  • src/static/hdd/hdd.service.ts
  • src/static/static.controller.spec.ts
  • src/static/static.controller.ts
  • src/static/static.interface.ts
  • src/static/static.service.spec.ts
  • src/static/static.service.ts
  • src/static/utils.spec.ts
  • src/static/utils.ts
  • src/test-runs/diffResult.ts
  • src/test-runs/dto/create-test-request-base64.dto.ts
  • src/test-runs/dto/create-test-request-multipart.dto.ts
  • src/test-runs/dto/create-test-request.dto.spec.ts
  • src/test-runs/dto/create-test-request.dto.ts
  • src/test-runs/dto/ignore-area.dto.spec.ts
  • src/test-runs/dto/ignore-area.dto.ts
  • src/test-runs/dto/testRun-paginated.dto.spec.ts
  • src/test-runs/dto/testRun.dto.ts
  • src/test-runs/dto/testRunResult.dto.spec.ts
  • src/test-runs/dto/testRunResult.dto.ts
  • src/test-runs/dto/update-ignore-area.dto.spec.ts
  • src/test-runs/dto/update-ignore-area.dto.ts
  • src/test-runs/test-runs.controller.spec.ts
  • src/test-runs/test-runs.controller.ts
  • src/test-runs/test-runs.service.spec.ts
  • src/test-runs/test-runs.service.ts
  • src/test-variations/test-variations.controller.ts
  • src/test-variations/test-variations.service.spec.ts
  • src/test-variations/test-variations.service.ts
  • src/users/db/dbusers.service.spec.ts
  • src/users/db/dbusers.service.ts
  • src/users/dto/change-password.dto.spec.ts
  • src/users/dto/change-password.dto.ts
  • src/users/dto/user.dto.ts
  • src/users/ldap/ldapusers.concurrency.spec.ts
  • src/users/ldap/ldapusers.service.spec.ts
  • src/users/ldap/ldapusers.service.ts
  • src/users/oidc/oidcusers.service.spec.ts
  • src/users/oidc/oidcusers.service.ts
  • src/users/users.controller.spec.ts
  • src/users/users.controller.ts
  • src/users/users.factory.spec.ts
  • src/users/users.factory.ts
  • src/users/users.interface.ts
  • src/users/users.service.spec.ts
  • src/users/users.service.ts
  • src/utils/bounded-cache.ts
  • src/utils/env.spec.ts
  • src/utils/env.ts
  • src/utils/index.spec.ts
  • src/utils/index.ts
  • src/utils/map-with-concurrency.ts
  • src/utils/random-key.ts
  • test/api-helpers.ts
  • test/body-limit.e2e-spec.ts
  • test/builds-api.e2e-spec.ts
  • test/builds.e2e-spec.ts
  • test/comparators.e2e-spec.ts
  • test/diff-worker-pool.e2e-spec.ts
  • test/jest.config.ts
  • test/ollama.e2e-spec.ts
  • test/preconditions.ts
  • test/project-favorites.e2e-spec.ts
  • test/projects-remove.e2e-spec.ts
  • test/projects.e2e-spec.ts
  • test/static.e2e-spec.ts
  • test/tasks.e2e-spec.ts
  • test/test-runs-review.e2e-spec.ts
  • test/test-runs-validation.e2e-spec.ts
  • test/test-runs.e2e-spec.ts
  • test/test-variations-api.e2e-spec.ts
  • test/test-variations.e2e-spec.ts
  • test/users-admin.e2e-spec.ts
  • test/users.e2e-spec.ts
  • test/validation-whitelist.e2e-spec.ts
  • test_acceptance/acceptance.spec.ts
  • test_acceptance/jest.config.ts
  • test_ldap/jest.config.ts
  • test_ldap/ldap.spec.ts
  • test_oidc/jest.config.ts
  • test_oidc/oidc.spec.ts
  • tsconfig.build.json
  • tsconfig.jest.json
  • tsconfig.json
  • tslint.json

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Deployments that never changed the sample secret kept starting before this
branch; an upgrade must not stop them.
Timeouts (Ollama, Gemini, S3, LDAP, Redis, diff job), VLM concurrency and the
build retention grace become constants. Foreign build ids are always accepted
with a warning, email-only tokens are always accepted, and the entrypoint drops
root whenever the volume allows it. None of these variables reached master.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant