Commit Graph

397 Commits

Author SHA1 Message Date
Arnas Donauskas
b8972fd905
Merge pull request #227 from ArnasDon/docs/changelog-default-currency
docs(changelog): note configurable default deal currency (#218)
2026-06-08 15:25:23 +03:00
Arnas Donauskas
d1b64ac0b8
docs(changelog): note configurable default deal currency (#218)
Adds the Unreleased entry for the per-account default currency under
Added, plus a "Migration required" note for 021 with the
apply-before-deploy warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:23:14 +03:00
Arnas Donauskas
e354bedd42
Merge pull request #226 from ArnasDon/feat/configurable-default-currency
feat(deals): make default deal currency configurable per account (#218)
2026-06-08 15:14:11 +03:00
Arnas Donauskas
c076b3cfe1
fix(currency): make formatCurrency total + add unit tests
deals.currency has no DB CHECK (only accounts.default_currency does),
so legacy rows, imports, or hand-edited data can hold a structurally
invalid code (e.g. "United States"). Intl.NumberFormat throws a
RangeError on such codes, which would crash any deal-card / contact
render that passes deal.currency straight through.

Guard formatCurrency with a try/catch that falls back to "CODE 1,234"
so a bad value degrades gracefully instead of taking down the view.
Add currency.test.ts locking in the defaults, the no-throw contract,
and formatCurrencyShort's symbol/fallback behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:10:56 +03:00
Arnas Donauskas
3abecb433b
feat(deals): make default deal currency configurable per account (#218)
wacrm hardcoded USD across every deal-value formatter, the new-deal
form, and automation-created deals, making deal tracking unhelpful for
non-US self-hosted users. Add a per-account default currency.

- Migration 021: accounts.default_currency TEXT NOT NULL DEFAULT 'USD'
  with a 3-letter-code CHECK. The existing admin-only accounts_update
  RLS policy already gates writes.
- New src/lib/currency.ts: single source of truth (CURRENCIES list,
  formatCurrency, formatCurrencyShort), replacing ~6 duplicated
  USD-hardcoded helpers.
- useAuth exposes defaultCurrency (joined from accounts), falling back
  to USD while loading / on pre-021 schemas.
- Settings → Deals tab to pick the currency (admin-only, read-only for
  others to match RLS).
- Deal form defaults new deals to the account currency and sources its
  dropdown from CURRENCIES; existing deals keep their saved currency.
- Pipeline board/analytics, dashboard "Open Deals Value", and the
  pipeline donut format totals in the account currency. Individual
  deal cards still show each deal's own currency.
- Automation create_deal sets the new deal's currency from the
  account default instead of the static DB default.

One currency per account (no FX conversion) per the issue's
recommended first pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:59:48 +03:00
Arnas Donauskas
11196b4728
Merge pull request #224 from ArnasDon/dependabot/npm_and_yarn/dev-dependencies-68c6bd2d2e
chore(deps-dev): bump @types/node from 25.9.1 to 25.9.2 in the dev-dependencies group
2026-06-08 13:46:59 +03:00
Arnas Donauskas
64eae5e0ab
Merge pull request #225 from ArnasDon/dependabot/npm_and_yarn/date-fns-4.4.0
chore(deps): bump date-fns from 4.3.0 to 4.4.0
2026-06-08 13:42:29 +03:00
Arnas Donauskas
e82400b221
Merge pull request #223 from ArnasDon/dependabot/npm_and_yarn/supabase-348e91dc31
chore(deps): bump @supabase/supabase-js from 2.106.2 to 2.107.0 in the supabase group
2026-06-08 13:40:16 +03:00
dependabot[bot]
13e03ab937
chore(deps): bump date-fns from 4.3.0 to 4.4.0
Bumps [date-fns](https://github.com/date-fns/date-fns) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/date-fns/date-fns/releases)
- [Commits](https://github.com/date-fns/date-fns/compare/v4.3.0...v4.4.0)

---
updated-dependencies:
- dependency-name: date-fns
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 10:35:38 +00:00
dependabot[bot]
df3250c518
chore(deps-dev): bump @types/node in the dev-dependencies group
Bumps the dev-dependencies group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 25.9.1 to 25.9.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 10:35:30 +00:00
dependabot[bot]
926fa4d043
chore(deps): bump @supabase/supabase-js in the supabase group
Bumps the supabase group with 1 update: [@supabase/supabase-js](https://github.com/supabase/supabase-js/tree/HEAD/packages/core/supabase-js).


Updates `@supabase/supabase-js` from 2.106.2 to 2.107.0
- [Release notes](https://github.com/supabase/supabase-js/releases)
- [Changelog](https://github.com/supabase/supabase-js/blob/master/packages/core/supabase-js/CHANGELOG.md)
- [Commits](https://github.com/supabase/supabase-js/commits/v2.107.0/packages/core/supabase-js)

---
updated-dependencies:
- dependency-name: "@supabase/supabase-js"
  dependency-version: 2.107.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: supabase
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 10:35:14 +00:00
Arnas Donauskas
a6839c244b
Merge pull request #222 from ArnasDon/fix/automation-builder-resource-pickers
fix(automations): pick tags, agents & templates by name in the builder
2026-06-08 13:23:20 +03:00
Arnas Donauskas
efd72b131e
fix(automations): pick tags, agents & templates by name in the builder
The automation builder exposed several developer-facing fields that
made automations hard to configure from the UI (issue #216):

- Keyword Match parsed/trimmed on every keystroke, so typing a
  comma-separated, multi-word list like "SEO, search engine
  optimization" was impossible — the comma and trailing space were
  stripped immediately.
- Tag Added / Add Tag / Remove Tag asked for a raw tag_id.
- Assign Conversation → Specific agent asked for a raw agent_id.
- Send Template asked for a manually typed template name + language.

Changes:
- Keyword input keeps a local draft and only parses into the keywords
  array on blur / Enter, then re-displays the cleaned form.
- New tag / agent / template pickers select existing resources by name
  while still storing ids / names internally, so the stored config
  shape (and the engine/validation) is unchanged. Resources load once
  at the builder root and are shared via context.
- Tags & approved templates load from Supabase (RLS-scoped, matching
  the broadcast picker); members load from /api/account/members.
- Every picker falls back to its original raw input when its list is
  empty (fresh account / older deployment), so automations stay
  authorable. Saved-but-missing ids are preserved when editing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:21:44 +03:00
Arnas Donauskas
151a5dae95
Merge pull request #221 from ArnasDon/fix/pipeline-board-horizontal-scroll
fix(pipelines): keep horizontal scrollbar visible on desktop boards
2026-06-08 13:10:14 +03:00
Arnas Donauskas
165bdd3335
fix(pipelines): keep horizontal scrollbar visible on desktop boards
The pipeline board uses overflow-x-auto but hid the scrollbar on fine
pointers, assuming the full board always fits on lg+. That assumption
breaks once a pipeline has many stages: columns keep a 260px min-width,
so the row overflows the viewport and the rightmost stage gets clipped
with no scrollbar or affordance to reveal it (issue #214).

Scope the scrollbar-hiding to touch devices (where the snap/peek layout
already hints at more content) and show a thin, themed horizontal
scrollbar on desktop so the overflow is discoverable and usable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:08:27 +03:00
Arnas Donauskas
934dcafffd
Merge pull request #211 from ArnasDon/fix/flows-pipelines-account-id
fix(account-sharing): stamp account_id on flow & pipeline inserts (#210)
2026-06-05 16:09:41 +03:00
Arnas Donauskas
48fc10d084
fix(account-sharing): stamp account_id on flow and pipeline inserts (#210)
`flows.account_id` and `pipelines.account_id` are NOT NULL post-017 with
no DB default, so inserts omitting account_id fail with a not-null
violation (same class as #206 / #205).

- POST /api/flows: resolve the caller's profiles.account_id and stamp it
  on both insert paths (template clone + blank/manual). Closes #210.
- Pipelines page: stamp account_id (from useAuth) on the auto-seed and
  the manual "create pipeline" inserts — same bug, found while auditing
  other account-scoped insert flows.

Audited every insert into the 15 NOT-NULL-account_id tables; all other
sites already stamp account_id. typecheck + 370 tests pass; lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 16:06:47 +03:00
Arnas Donauskas
8bf7ee097e
Merge pull request #209 from ArnasDon/fix/dependabot-transitive-security-bumps
fix(security): patch vulnerable transitive deps (hono, qs, brace-expansion)
2026-06-05 15:14:59 +03:00
Arnas Donauskas
0a2abc4290
fix(security): bump vulnerable transitive deps (hono, qs, brace-expansion)
Resolves all 6 open Dependabot alerts (#23-#28), all moderate severity:

- hono 4.12.19 -> 4.12.23 (>=4.12.21 fixes #25-#28: Set-Cookie injection,
  app.mount() path decoding, IPv6 deny-rule bypass, JWT scheme validation).
  Bumps the existing `hono` override floor to ^4.12.21.
- qs 6.15.1 -> 6.15.2 (fixes #24: stringify DoS on null/undefined comma-array entries).
- brace-expansion 5.0.5 -> 5.0.6 (fixes #23: large numeric range defeats max DoS protection).

All transitive (lockfile-only) deps. typecheck + 370 tests pass; npm audit clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:13:07 +03:00
Arnas Donauskas
75ff74730f
Merge pull request #208 from ArnasDon/fix/theme-hydration-warning
fix(theme): suppress hydration warning on root html data-theme (#207)
2026-06-05 14:14:57 +03:00
Arnas Donauskas
7c373c8e0a
fix(theme): suppress hydration warning on root html data-theme (#207)
The `theme-boot` beforeInteractive script in layout.tsx rewrites
`data-theme` on <html> from localStorage before React hydrates, so for
any non-default theme the client DOM intentionally differs from the
server-rendered DEFAULT_THEME ("violet"). React then logged a hydration
mismatch on <html> on every load where the saved theme differed.

Add suppressHydrationWarning to the root <html>. It's scoped to that
element's own attributes (child mismatches still surface) and preserves
the documented localStorage-only/device-scoped theme design — unlike a
cookie-based SSR approach, which would change that behavior.

Verified in-browser (Next 16.2.6 / Turbopack): with a non-default theme
in localStorage, the reproduced "+ data-theme=violet / - data-theme=cobalt"
hydration error is gone; removing the prop brings it back (negative
control). typecheck, lint (0 errors), 370 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:46:23 +03:00
Arnas Donauskas
0a9e2db61a
Merge pull request #206 from ArnasDon/fix/account-id-inserts
fix(account-sharing): stamp account_id on account-scoped inserts (#205)
2026-06-05 10:25:18 +03:00
Arnas Donauskas
c5ee04ce9e
fix(account-sharing): stamp account_id on account-scoped inserts (#205)
Migration 017 made domain tables account-scoped: account_id is NOT NULL
and INSERT RLS is WITH CHECK (is_account_member(account_id, ...)). Several
client- and server-side create flows still inserted only user_id, so on a
fresh install they failed with 403 / "new row violates row-level security
policy" / not-null violations.

Resolve account_id from useAuth() (client) or the caller's profile (server)
and include it on every account-scoped insert:

- contacts: contact-form, import-modal, broadcast CSV upsert
- contact_notes: contact-sidebar, contact-detail-view
- tags: tag-manager
- broadcasts: broadcasts/new page, use-broadcast-sending
- deals: deal-form
- automations: POST /api/automations, POST /api/automations/[id]/duplicate

Each path also guards on a missing accountId with a clear message instead
of letting the insert fail opaquely.

pipelines/page.tsx and api/flows/route.ts are intentionally left to the
already-open PR #193, which fixes those same paths (and adds tests).

Verified: a full sweep of every account-scoped insert/upsert across src/
now includes account_id (except the two paths owned by #193). typecheck,
lint (0 errors), and 370 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:20:54 +03:00
Arnas Donauskas
d6dbbf22b2
Merge pull request #204 from ArnasDon/ga/members-account-sharing
feat(members): GA the Members tab (remove account_sharing beta gate)
2026-06-04 17:42:27 +03:00
Arnas Donauskas
5332f77831
feat(members): GA the Members tab — remove account_sharing beta gate
The multi-user account-sharing surface (Members tab + invite/role
management APIs + /join redeem flow) is complete, so drop the
`account_sharing` beta flag that hid it during development. Mirrors
the Flows soft-GA in 0.2.0.

- settings: Members tab is now always rendered (no beta_features
  check, no stale-link fallback). The `profiles.beta_features`
  column itself stays for future betas.
- sidebar: the active-account strip no longer keys off the flag.
  It now shows whenever the account name differs from the user's
  own name — i.e. once you've renamed the account or joined a
  shared one — preserving the original "don't duplicate a solo
  user's name" behaviour without the flag.
- CHANGELOG/README: document Members as a shipped, all-users
  feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:39:46 +03:00
Arnas Donauskas
80b4639c60
Merge pull request #195 from ArnasDon/dependabot/npm_and_yarn/dev-dependencies-4825ac1e2e
chore(deps-dev): bump vitest from 4.1.7 to 4.1.8 in the dev-dependencies group
2026-06-04 16:50:09 +03:00
Arnas Donauskas
11efb8a785
Merge pull request #197 from ArnasDon/dependabot/npm_and_yarn/shadcn-4.10.0
chore(deps): bump shadcn from 4.2.0 to 4.10.0
2026-06-04 16:48:17 +03:00
Arnas Donauskas
5c262ae3ee
Merge pull request #198 from ArnasDon/dependabot/npm_and_yarn/lucide-react-1.17.0
chore(deps): bump lucide-react from 1.16.0 to 1.17.0
2026-06-04 16:45:22 +03:00
Arnas Donauskas
74b590eaf9
Merge pull request #199 from ArnasDon/dependabot/npm_and_yarn/xyflow/react-12.11.0
chore(deps): bump @xyflow/react from 12.10.2 to 12.11.0
2026-06-04 16:39:51 +03:00
Arnas Donauskas
47987acb06
Merge pull request #163 from dhruv465/codex/fix-cache-flow-canvas-drag
fix flow drag responsiveness and stale chunks
2026-06-02 13:39:03 +03:00
dependabot[bot]
438cb28162
chore(deps): bump @xyflow/react from 12.10.2 to 12.11.0
Bumps [@xyflow/react](https://github.com/xyflow/xyflow/tree/HEAD/packages/react) from 12.10.2 to 12.11.0.
- [Release notes](https://github.com/xyflow/xyflow/releases)
- [Changelog](https://github.com/xyflow/xyflow/blob/main/packages/react/CHANGELOG.md)
- [Commits](https://github.com/xyflow/xyflow/commits/@xyflow/react@12.11.0/packages/react)

---
updated-dependencies:
- dependency-name: "@xyflow/react"
  dependency-version: 12.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 07:07:33 +00:00
dependabot[bot]
972762a9dd
chore(deps): bump lucide-react from 1.16.0 to 1.17.0
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.16.0 to 1.17.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.17.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 07:07:22 +00:00
dependabot[bot]
0bff06b8e6
chore(deps): bump shadcn from 4.2.0 to 4.10.0
Bumps [shadcn](https://github.com/shadcn-ui/ui/tree/HEAD/packages/shadcn) from 4.2.0 to 4.10.0.
- [Release notes](https://github.com/shadcn-ui/ui/releases)
- [Changelog](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/CHANGELOG.md)
- [Commits](https://github.com/shadcn-ui/ui/commits/shadcn@4.10.0/packages/shadcn)

---
updated-dependencies:
- dependency-name: shadcn
  dependency-version: 4.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 07:07:10 +00:00
dependabot[bot]
47439660aa
chore(deps-dev): bump vitest in the dev-dependencies group
Bumps the dev-dependencies group with 1 update: [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `vitest` from 4.1.7 to 4.1.8
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 07:06:49 +00:00
Arnas Donauskas
73041bfa64
Merge pull request #194 from ArnasDon/fix/automation-engine-cross-tenant-contact
fix(automations): enforce tenant isolation on caller-supplied contactId
2026-06-02 09:32:28 +03:00
Arnas Donauskas
b4f18537bb
fix(automations): enforce tenant isolation on caller-supplied contactId
The automation engine executes steps via the service-role client, which
bypasses RLS. POST /api/automations/engine reads contact_id straight from
the request body, so an authenticated attacker could supply another
tenant's contact UUID and have the engine modify it (update_contact_field),
tag it, or read it back through a condition — bypassing the contacts RLS
policy. (GHSA-63cv-2c49-m5v3)

- Primary: verify the supplied contactId belongs to the dispatching
  account before any step runs; refuse silently otherwise.
- Defense in depth: scope the service-role contacts writes/reads
  (update_contact_field, contact_field condition) by account_id. Tag
  steps rely on the entry-point guard since contact_tags has no
  account_id column — documented inline.
- Add engine.test.ts covering the guard (PoC inverted), the happy path,
  and the account-scoped write.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 23:36:37 +03:00
Arnas Donauskas
19a1df275a
Merge pull request #192 from ArnasDon/docs/readme-nodejs-deploy-banner
docs(readme): swap brand banner for custom Node.js deploy CTA
2026-06-01 16:38:02 +03:00
Arnas Donauskas
0c8f9a465a
docs(readme): swap brand banner for custom Node.js deploy CTA
Replace the generic Hostinger brand banner with a purpose-built
"Ship your Node.js app in one click — Deploy to Hostinger" banner
designed from the Hostinger design system (signature purple,
DM Sans Black, real Hostinger logo lockup, Node.js green hexagon
mark). Sized 1200×400 at 2× DPR for retina, ~122 KB PNG.

Implements the V1 "Purple baseline" variation from the design
package "Node.js Deploy Banner.html". Drops the small text caption
under the top banner and the redundant "Deploy your fork" shields.io
companion (the new banner has its own embedded CTA pill); keeps the
"Step-by-step guide" link since that still goes to docs, not
Hostinger.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 16:35:32 +03:00
Arnas Donauskas
32f9de903c
Merge pull request #191 from ArnasDon/docs/readme-hostinger-banner-image
docs(readme): use Hostinger banner as deploy CTA
2026-06-01 15:31:53 +03:00
Arnas Donauskas
25d165253d
docs(readme): use Hostinger banner as deploy CTA
Swap the shields.io text badges for the Hostinger logo banner — a real
brand image makes the deploy CTA the visual anchor of the README and
of the dedicated section. Add a small "→ Deploy wacrm on Hostinger"
caption under the top banner for clarity, and keep the small
shields.io companion buttons under the section banner so the
step-by-step guide stays one click away.

Asset committed to .github/assets/hostinger-deploy.webp (21 KB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 15:29:25 +03:00
Arnas Donauskas
d23a5a5017
Merge pull request #190 from ArnasDon/feat/rate-limit-admin-account-endpoints
feat(api): per-user rate limits on admin account endpoints
2026-06-01 15:12:49 +03:00
Arnas Donauskas
01715c7d6d
feat(api): per-user rate limits on admin account endpoints
Addresses the L1 finding from the final security review. The six
admin-class write endpoints under /api/account had no rate
limits, so a compromised admin session (or a script run in a
loop) could spam invite creation / role flips / member removes
fast enough to flood the DB or audit logs.

Adds a new `RATE_LIMITS.adminAction` bucket — 30/min/user — and
wires it into:

  - POST   /api/account/invitations          → admin:inviteCreate
  - DELETE /api/account/invitations/[id]     → admin:inviteRevoke
  - PATCH  /api/account/members/[userId]     → admin:memberRole
  - DELETE /api/account/members/[userId]     → admin:memberRemove
  - POST   /api/account/transfer-ownership   → admin:transferOwnership
  - PATCH  /api/account                      → admin:rename

Each endpoint keys its own bucket so spamming one doesn't starve
another. 30/min is comfortably above any realistic human pace
(the Members tab is a clicks-only UI; a legit admin would
struggle to hit ten of these per minute by hand) while still
bounding accidental abuse.

The bucket joins the existing send / broadcast / react /
invitationPeek / invitationRedeem buckets in `RATE_LIMITS` —
same in-memory fixed-window limiter, same `rateLimitResponse`
helper for the 429 shape.

Severity in the final review: LOW. The admin-trust model means
this isn't blocking — but the cost of adding it is six
near-identical 4-line additions and zero net risk, so worth
landing for hardening.

Verification
- `npm run lint`      → 0 errors (same 19 pre-existing warnings)
- `npm run typecheck` → clean
- `npm test`          → 366/366 pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 15:05:49 +03:00
Arnas Donauskas
fa95a32b2f
Merge pull request #189 from ArnasDon/docs/readme-hostinger-promo
docs(readme): expand Hostinger deploy promotion
2026-06-01 14:32:55 +03:00
Arnas Donauskas
1d0f14a7e3
docs(readme): expand Hostinger deploy promotion
Make the Hostinger CTA the visual anchor of the README and add a
dedicated "Deploy on Hostinger (recommended)" section with concrete
reasons (one-click Git deploy, Managed Node.js, free SSL, CDN, env
vars in hPanel, DDoS + backups, pricing, support) and a 60-second
deploy walkthrough. Bigger, centered HTML-scaled badges at the top
and in the section. Linked from the "Why fork this?" bullet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 14:28:58 +03:00
Arnas Donauskas
d75a9330a7
Merge pull request #188 from ArnasDon/harden/invite-allowed-hosts
harden(invite): optional ALLOWED_INVITE_HOSTS allow-list
2026-06-01 14:28:47 +03:00
Arnas Donauskas
44f5c550e1
harden(invite): optional ALLOWED_INVITE_HOSTS allow-list
Defense-in-depth for the request-derived branch of `getBaseUrl()`
in /api/account/invitations.

The threat model
- On a typical proxied deploy (Vercel / Hostinger / Cloudflare /
  nginx with a fixed server_name), the proxy overwrites the
  `Host` and `X-Forwarded-Host` headers with the deployment's
  canonical hostname, so the request-derived path produces the
  right URL. NEXT_PUBLIC_SITE_URL pinning is also unnecessary in
  that case.
- On a *bare* deployment exposed to the public internet (no proxy
  in front, no SITE_URL set), an attacker can POST directly to
  /api/account/invitations with `Host: phishing.example` and
  receive an invite URL pointing at their site. They'd still need
  admin role on *some* account to call the endpoint, and they'd
  need to set up `phishing.example/join/<token>` to actually
  phish anyone — but the resolution chain shouldn't trust
  un-validated client input by default.

The fix
- New env var `ALLOWED_INVITE_HOSTS` (comma-separated hostnames).
- When set, `getBaseUrl` validates each candidate host against
  the list and falls through to wacrm.tech if neither matches.
- When unset, behavior is unchanged — the request-derived host
  is trusted (preserves the "just works" experience for the
  typical proxied case).
- Distinct console.warn for the rejection path vs the
  no-Host-at-all path so operators can tell their allow-list is
  actually catching something.

Documented in .env.local.example with a clear explanation of
when operators should set it (bare deploys + multi-tenant
hosting) and when they probably don't need to (the typical case
where NEXT_PUBLIC_SITE_URL already pins the host).

Severity: MEDIUM in the final review, downgraded in practice
because (a) the attack window is narrow, (b) requires admin
role + a phishing site, and (c) most deployments are behind a
proxy that strips the spoofable headers. Still worth the small
amount of code to give security-conscious operators the knob.

Verification
- `npm run lint`      → 0 errors (same 19 pre-existing warnings)
- `npm run typecheck` → clean
- `npm test`          → 366/366 pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 14:26:01 +03:00
Arnas Donauskas
c247fd0a9b
Merge pull request #187 from ArnasDon/fix/whatsapp-config-shared-by-account
fix(multi-user): share whatsapp_config + templates by account
2026-06-01 14:05:45 +03:00
Arnas Donauskas
2e911d5e5a
fix(multi-user): share whatsapp_config + templates by account
Reported by @ArnasDon: a teammate accepting an invite landed in the
shared account but the WhatsApp Config tab still showed "not
connected." Inbox banner read "WhatsApp not connected" too.

Cause: PR 8 of the multi-user series converted the *webhook* and
the *automations/flows engines* to account-scoped lookups, but
left every user-facing API route + UI page still filtering by
`user_id`. Post-017 `whatsapp_config` and `message_templates` are
account-scoped (one row per account, with `account_id NOT NULL`),
so `.eq('user_id', user.id)` returned zero rows for any teammate
who didn't personally save the config — including everyone who
joined via invite.

This PR completes the conversion across the user-facing surface.

Surface (11 files)
- `src/components/settings/whatsapp-config.tsx` — read accountId
  from `useAuth()`; trigger fetch on profile-ready, not just
  user-ready; pass accountId through to the supabase query.
- `src/app/(dashboard)/inbox/page.tsx` — resolve account_id from
  profiles, query whatsapp_config by account_id for the
  connected-banner state.
- `src/app/api/whatsapp/config/route.ts` — GET/POST/DELETE all
  resolve account_id from profile. The phone_number_id collision
  check (service-role) now compares against `account_id`, not
  `user_id` — two accounts still can't share a Meta number, two
  teammates in one account share one row by design. INSERT
  populates `account_id` (tenancy) + `user_id` (audit).
- `src/app/api/whatsapp/config/verify-registration/route.ts` —
  same conversion.
- `src/app/api/whatsapp/send/route.ts` — conversation,
  whatsapp_config, message_templates lookups all switch to
  account_id; rate-limit key stays per-user.
- `src/app/api/whatsapp/react/route.ts` — conversation +
  whatsapp_config switch to account_id.
- `src/app/api/whatsapp/broadcast/route.ts` — whatsapp_config +
  message_templates + broadcasts switch to account_id.
- `src/app/api/whatsapp/media/[mediaId]/route.ts` — config
  lookup switches to account_id.
- `src/app/api/whatsapp/templates/submit/route.ts` — config
  lookup + every message_templates SELECT/upsert now uses
  account_id. The upsert ROW gains `account_id` so the
  NOT NULL constraint added by migration 017 stops rejecting
  new templates (this was an incidental bug — every new
  template post-017 would have been broken regardless of
  multi-user).
- `src/app/api/whatsapp/templates/sync/route.ts` — same.
- `src/app/api/whatsapp/templates/[id]/route.ts` PATCH +
  DELETE — same.

Pattern at every route:

  const { data: profile } = await supabase
    .from('profiles')
    .select('account_id')
    .eq('user_id', user.id)
    .maybeSingle()
  const accountId = profile?.account_id as string | undefined
  if (!accountId) return 403

then every downstream `.eq('user_id', user.id)` → `.eq('account_id', accountId)`.

Known follow-up not in this PR
- `message_templates` still has its UNIQUE index on
  `(user_id, name, language)` from migration 014; the submit
  upsert's `onConflict` targets that same triple. As a result,
  two teammates can still INSERT same-named templates without
  conflicting. The fix needs a small migration (drop legacy
  index, add `(account_id, name, language)` index, switch the
  `onConflict` target) — TODO comment left in the route file so
  it's not forgotten.

Verification
- `npm run lint`      → 0 errors (same 19 pre-existing warnings)
- `npm run typecheck` → clean
- `npm test`          → 366/366 pass
- `npm run build`     → succeeds

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 14:01:46 +03:00
Arnas Donauskas
7aac97e3ef
Merge pull request #186 from ArnasDon/fix/invite-url-origin
fix(invite): derive invite-URL origin from the request, not wacrm.tech
2026-06-01 09:19:15 +03:00
Arnas Donauskas
7d466ca865
fix(invite): derive invite-URL origin from the request, not wacrm.tech
Pre-fix: `getBaseUrl()` in /api/account/invitations defaulted to
`https://wacrm.tech` whenever `NEXT_PUBLIC_SITE_URL` was unset.
wacrm.tech is the docs/marketing site (separate ArnasDon/wacrm-site
repo); it has no `/join/[token]` route. Any fork that didn't set the
env var got invite links pointing into the marketing site, which
404'd on click. Reported by @ArnasDon while testing the flow on
production.

Post-fix: `getBaseUrl(request)` resolves in this order:

  1. `NEXT_PUBLIC_SITE_URL` — explicit operator config, still wins.
  2. `X-Forwarded-Host` (+ `X-Forwarded-Proto`) — set by every
     reverse proxy in front of the app (Hostinger Managed Node.js,
     Vercel, Cloudflare). This is what makes invite links Just
     Work in production without any env-var fiddling.
  3. `Host` header + the request's protocol — bare deployments
     without a proxy in front.
  4. `https://wacrm.tech` last-resort fallback with a console.warn
     so operators spot the misconfig in logs. Effectively
     unreachable from a real browser; only triggers if there's no
     Host header at all.

Also tightened `.env.local.example` so the variable's purpose
matches the new behavior: it's now only needed when the request-
derived origin would be wrong (cron jobs, background workers).
Day-to-day forks don't have to remember it.

Verification
- `npm run lint`      → 0 errors (same 19 pre-existing warnings)
- `npm run typecheck` → clean (after `rm -rf .next` to clear
                        stale [locale]/* type stubs from the
                        i18n revert that landed on main)
- `npm test`          → 366/366 pass
- `npm run build`     → succeeds

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 09:16:14 +03:00