Commit Graph

8 Commits

Author SHA1 Message Date
Arnas Donauskas
738db87847
fix(auth): load account context without an embedded FK join
getCurrentAccount() and the client AuthProvider resolved the caller's
account through a PostgREST embedded join (`account:accounts!inner(...)`)
on profiles. That embed requires PostgREST to have the
profiles.account_id -> accounts.id relationship in its schema cache.
When the cache is stale -- a common Supabase state right after a
migration adds the FK, or when migrations are applied out of band -- the
embed fails hard with PGRST200 and the whole account context collapses.

It is the only query shape in the app that depends on relationship
inference (everything else reads accounts with plain `.eq('id', ...)`
filters), which is why the dashboard kept working while Settings ->
API keys / Team members returned 403 "Could not load account context"
and the Overview's cards hung loading (issue #294).

Replace the embed with a plain account-by-id lookup in both the server
helper and the client AuthProvider. Same accounts RLS, no relationship
inference, graceful degradation. The client now also keeps account_id /
account_role even if the account-name lookup itself fails. Adds a
regression test for getCurrentAccount.

Fixes #294

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:34:26 +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
7593253c85
feat(auth): extend useAuth with account context + RequireRole + useCan
PR 5 of the multi-user accounts series. Surfaces the account
+ role state from the profile row through useAuth so every UI
gate downstream can read it without its own fetch, and lands
two reusable patterns the upcoming Members tab / Join page /
sidebar gating PRs will all consume.

What's new in useAuth
- Profile SELECT now includes `account_id`, `account_role`, and
  the joined `accounts(id, name)` via the existing `account_id`
  FK. `!inner` mirrors the server-side `getCurrentAccount`
  helper from PR #169.
- Context value adds `accountId`, `accountRole`, `account`
  (`{id, name}` summary), and six derived booleans: `isOwner`,
  `isAdmin`, `isAgent`, `isViewer`, plus the capability-level
  `canManageMembers`, `canEditSettings`, `canSendMessages`.
- All derived booleans memoise off `profile.account_role`, so
  consumers can use them as React dep-array entries without
  re-renders cascading on unrelated profile fields.
- The no-provider fallback returns every new field at its
  least-privileged value (null / false) — UI gates fail closed
  if a component is somehow rendered outside the AuthProvider.

`<RequireRole min="admin" fallback={…}>{children}</RequireRole>`
- Renders `children` iff `accountRole >= min`, else `fallback`.
- Treats `profileLoading` as "below threshold" so under-
  privileged users never see a flash of gated content during
  the initial profile fetch.

`useCan(action)` hook
- Typed action union: `manage-members` | `edit-settings` |
  `send-messages` | `view-only` | `delete-account` |
  `transfer-ownership`. Closed set lets the compiler catch
  typos at every call site.
- Returns `false` while loading. The boolean alternative to
  `<RequireRole>` — useful for `disabled={!useCan('edit-settings')}`
  on buttons / inputs / tooltip copy without restructuring JSX.

Why no React-renderer tests
- The wacrm test convention is pure unit tests (`vitest` without
  any React testing library). The role predicates that back
  every new piece here are already covered by the test suite
  added in PR #169 — the new code is thin glue, and adding a
  React testing harness for it would be a much larger change
  than this PR warrants.

Verification
- `npm run lint`      → 0 errors. Warnings drop from 20 → 19
                        because the rewritten useEffect now
                        lists `fetchProfile` in its deps.
- `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-05-31 17:58:53 +03:00
Arnas Donauskas
4e89b2b902
feat(flows): soft-GA — open to all users + Beta label
The Flows feature was previously gated on a per-account
`profiles.beta_features` opt-in: the sidebar entry was invisible
to non-beta accounts, every /api/flows route 404'd, the webhook
dispatcher was a no-op, and the agent-handoff pause was skipped.

This PR opens the surface to every authenticated user. A small
"Beta" chip in the sidebar nav + on the /flows page H1 keeps the
"young feature" signal without holding anyone out.

Removed:
- `src/lib/flows/feature-flag.ts` + its tests (-11 tests).
- The `requireFlowsBeta` / `requireFlowsBetaAndOwnership` helpers
  in the API routes — collapsed to plain `requireUser` / RLS
  ownership.
- The `isProfileFlowsEnabled` helper + branch in the webhook
  dispatcher — the runner takes the cheap "no_match" exit on
  accounts with no active flows (single indexed SELECT).
- The agent-handoff pause gate in /api/whatsapp/send — the UPDATE
  matches zero rows when no active run exists, also cheap.
- Client-side beta gates in the 3 flows pages (and the redundant
  `useAuth` + `profileLoading` plumbing they needed).
- The defensive message-bubble gate that would have rendered
  interactive replies as plain text for non-beta accounts.
- The `betaKey` field on the sidebar NavItem schema, replaced by a
  simpler boolean `beta` that purely drives the visual chip.

Kept:
- `profiles.beta_features` column. Generic mechanism for future
  beta gates; no current feature reads it. Comments in
  `src/types/index.ts` + `use-auth.tsx` updated to say so.
- The chip can be removed in a one-liner once the team's satisfied
  Flows has bedded in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 17:16:58 +03:00
Arnas Donauskas
6fcf77de8c
fix(flows): bounce-to-dashboard race when navigating into /flows
use-auth's `loading` flips false as soon as the session settles —
the profile fetch is fire-and-forget so the page renders quickly
with just the user object. The flows pages, though, gate access on
`isFlowsEnabled(profile.beta_features)`. During the gap between
session-ready and profile-ready they saw `{ loading: false,
profile: null }` and read it as "not opted in", redirecting a
legitimate beta user back to /dashboard.

Expose a separate `profileLoading` boolean from the auth context
that stays true until fetchProfile settles (success, missing row,
or error). Pages that branch on `profile.*` gate on it; chrome
that only needs the user object keeps using `loading` as before.

Three call sites updated: /flows, /flows/[id], /flows/[id]/runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:05:59 +03:00
Arnas Donauskas
9c4a46d990
feat(beta): per-account beta_features flag on profiles, gating Flows
Scaffolding for the upcoming WhatsApp Flows feature. The whole Flows
stack (interactive button/list send + parse, server-side runner, no-
code builder, templates) will land across the next ~4 PRs *behind*
this gate so the owner can dogfood the feature on their own account
before exposing it to teammates or downstream pullers of the OSS.

What this lands:

  1. `profiles.beta_features TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[]`
     — generic opt-in column, lives on the existing profiles table.
     Empty by default for every existing row.

  2. `src/lib/flows/feature-flag.ts` — `isFlowsEnabled(profile)`
     predicate + `BETA_FEATURE_FLOWS` constant. Single source of
     truth for the gate; later PRs import this in every guarded
     surface (sidebar nav, /flows page, /api/flows routes, webhook
     dispatcher).

  3. `Profile.beta_features` added to the shared TS type and the
     auth hook's local copy. Auth hook loads the column and
     defaults it to `[]` defensively if the DB read returns null.

  4. 11 unit tests covering null/undefined/empty/mismatched cases,
     including case sensitivity (only lowercase `'flows'` opts in)
     and defensive guards against malformed input.

How the owner enables it for their own account, once merged:

    UPDATE profiles
    SET beta_features = array_append(beta_features, 'flows')
    WHERE user_id = '<their-auth-uid>';

How we ship it to everyone, when ready: grep-and-delete the
`isFlowsEnabled()` callsites in one cleanup PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:57:33 +03:00
Arnas Donauskas
72a5e4cf70
feat(settings): user profile management
Adds a profile tab to /settings with the five things every B2B SaaS
user expects:

- Display name — edits profiles.full_name.
- Avatar — upload / replace / remove, stored in a new `avatars`
  Supabase Storage bucket with RLS scoped to the owner's folder.
  Migration 008_profile_avatars_storage.sql creates the bucket (public
  read, 2 MB cap, image mimes only) and the four storage.objects
  policies.
- Email — goes through supabase.auth.updateUser({ email }), which
  confirms to both the old and new addresses. UI surfaces a "check
  both inboxes" banner after save.
- Password — three-field form with a current-password gate implemented
  via a throwaway signInWithPassword() before calling updateUser.
- Active sessions — one-click "sign out of all devices" using
  supabase.auth.signOut({ scope: 'global' }), with a confirm dialog.

Plus read-only role, joined date, and user id for operator debugging.

The header avatar and sidebar user row are now both dropdown menus
linking to /settings?tab=profile and /settings?tab=whatsapp; the
settings page reads ?tab= to land on the right tab. useAuth gets a
refreshProfile() callback so the header/sidebar update immediately
after a save without a full reload.

Deferred (separate work): account deletion, 2FA, phone/timezone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:40:39 +03:00
Arnas Donauskas
186c9f0e1e
fix: share auth via React Context to prevent getSession() timeouts
Fixes the cascading warnings:
  [useAuth] getSession() timed out after 3s, clearing loading state
  [useAuth] getSession() timed out after 3s, clearing loading state
  [useAuth] getSession() timed out after 3s, clearing loading state

The warning appeared 3x per page load because 3 components each called
useAuth() independently (DashboardLayout, Sidebar, Header), each firing
their own supabase.auth.getSession() call. The Supabase client has
internal mutex locks around session reads, so concurrent calls from
multiple components queue up and timeout.

Fix: introduce an AuthProvider React Context that calls getSession()
ONCE for the whole subtree. useAuth() now reads from context instead
of making its own call.

Also fixes the Settings page stuck on loading: WhatsAppConfig,
TemplateManager, and TagManager were making their own getSession()
calls in fetch functions. They now read the user from useAuth()
context — no redundant session calls.

Changes:
- src/hooks/use-auth.ts -> use-auth.tsx (JSX for the Provider)
  Added AuthProvider component and converted useAuth() to a
  context consumer. Renamed because the file now exports JSX.
- src/app/(dashboard)/layout.tsx — wraps children in <AuthProvider>
  with a DashboardShell inner component that consumes the context.
- src/components/settings/whatsapp-config.tsx — uses useAuth() for
  user instead of calling getSession() itself.
- src/components/settings/template-manager.tsx — same.
- src/components/settings/tag-manager.tsx — same.
- Profile fetch is now fire-and-forget (doesn't block setLoading),
  so the UI renders as soon as the session is known.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 20:26:09 +03:00