Download Latest Version v0.4.0 -- Security fix + behavioral change_ manager hierarchy visibility source code.zip (1.3 MB)
Email in envelope

Get an email when there's a new version of openestate

Home / v0.3.1
Name Modified Size InfoDownloads / Week
Parent folder
README.md 2026-08-18 3.9 kB
v0.3.1 -- Security fix_ credential leak in inquiry_follow-up endpoints source code.tar.gz 2026-08-18 963.7 kB
v0.3.1 -- Security fix_ credential leak in inquiry_follow-up endpoints source code.zip 2026-08-18 1.3 MB
Totals: 3 Items   2.3 MB 0

First real pilot-user feedback, triaged and fixed. Three critical bugs (hit hourly by a working sales team), two "check before building" items that turned out to be backend-complete, and a security leak found along the way.

Security

  • Credential leak, fixed: staff-facing inquiry and follow-up endpoints returned password hashes, TOTP secrets, and recovery codes to any authenticated staff user. GET /inquiries, GET /inquiries/:id, and GET /inquiries/:id/follow-ups embedded the full User row for assignedTo/createdBy — including passwordHash, totpSecret, and recoveryCodes — because a bare Prisma include: { relation: true } returns every scalar column on the related model, not just the ones the UI displays. Any staff user with read access to inquiries (not just admins) could see this data in their browser's network tab on every list/detail/follow-up load. Fixed by scoping both call sites (InquiryService.findAll/ findOne, FollowUpService.findAllForInquiry) to explicit selects ({id, name, email} / {id, name}), verified absent both in a through-the-wire regression test and by inspecting raw response JSON on a live deployed instance. If you are running v0.3.0 or earlier, upgrade to v0.3.1 and rotate any staff password or TOTP secret you consider exposed — anyone who had inquiry-read access and inspected network traffic (or logs, if any client/proxy captured response bodies) during that window could have captured these values.

Fixed

  • A rep's own inquiry could silently reassign to admin. InquiryService.create() ran round-robin unconditionally whenever a project was set, with no notion of who created the inquiry — Inquiry had no createdById column at all. New default policy: an interactively-created inquiry is assigned straight to its creator; round-robin only runs for machine-driven intake (inbound lead API, bulk import), which has no human creator to retain ownership for. Configurable via CompanyConfig.presalesCreatorRetainsLead (default on).
  • Editing a user, for any field, always failed. UserForm.tsx's edit-mode submit sent a create-shaped payload (including email) to a .strict() update endpoint that never declared it — every save 400'd. A deeper client-side bug sat in front of it: the form's validation resolver was always createUserSchema, which requires password — edit mode never renders that field, so submission failed silently at the validation layer before a request was ever sent. Both fixed; a new shared pickForSchema() helper (projects onto an update schema's own declared keys, rather than subtracting fields off a create-shaped object) closes the class of bug, applied at all three known sites (this one, Masters, Broker commission payment) plus a regression test covering every create/update schema pair this package exports.
  • Direct URL access to an admin page rendered the full page shell for any authenticated user, regardless of permission — the backend correctly 403'd the underlying data fetch, but nothing distinguished "no data yet" from "not allowed here." Every protected route now renders an access-denied screen instead when the user lacks the permission that gates it.

Added

  • Bulk Excel inquiry import now has a staff UI. The backend (row-level validation, applicant dedup, ExcelJS-based parsing) has existed since Phase 3 with no caller in apps/web — upload UI, a downloadable XLSX template (GET /inquiries/import-template, single source of truth shared with the parser's own header map), and per-row error reporting added to the Inquiries page.
  • Follow-up log now shows who logged it. FollowUp.createdById has been captured since Phase 3 and was already being fetched — just never rendered.
Source: README.md, updated 2026-08-18