Commit Graph

9 Commits

Author SHA1 Message Date
9067ab8f0c fix(auth): also default role/aud on auth.users UPDATE, not just INSERT
GoTrue v2.158.1 emits an UPDATE right after inserting an OIDC-provisioned
user (pop ORM resends the full struct incl. the zero-value role=''),
which bypassed migration 013's BEFORE INSERT trigger and left prod users
with role='' — breaking every PostgREST call with `role "" does not exist`.
Add a matching BEFORE UPDATE trigger reusing the same guard function, and
re-backfill the rows already clobbered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:51:21 +02:00
2294a31942 fix(auth): default empty role/aud on auth.users → 'authenticated'
Found on ambrosio after the first real self-registration: GoTrue v2.158.1
persists OIDC-provisioned users with empty-string role and aud. PostgREST
then dies with `role "" does not exist` on every REST call because its
per-request `SET LOCAL role = $claim` can't resolve ''. Dev never caught
this because seed.sql hardcodes both columns.

- supabase/migrations/013_auth_users_role_default.sql: backfill existing
  rows + BEFORE INSERT trigger on auth.users that fills empty/NULL role +
  aud with 'authenticated'. Idempotent, preserves explicit values.
- supabase/tests/008_auth_user_role_default.sql: pgTAP coverage — function
  + trigger exist, backfill left nothing empty, empty-string and NULL
  values get defaulted, explicit non-empty role is preserved.
- CLAUDE.md: status line updated to 255 tests (was 248), new gotcha #19
  documenting the GoTrue quirk + that users in existing sessions must
  re-login once for a fresh JWT after the fix lands on any given env.

Total: 41 pgTAP (was 34) + 140 integration + 15 unit + 58 Playwright + 1
gated rate-limit. Full `just test-all` green.
2026-04-15 00:23:00 +02:00
f5c8cb68be feat(fase-7): collective-flow E2E coverage + fix 3 latent bugs
12 new Playwright tests closing the UI coverage gap in the collective
lifecycle (onboarding → invitation → admin manage). Writing them surfaced
three product-code bugs that have silently been present since Fase 1;
fixed as part of this phase.

Tests:
- tests/e2e/onboarding.test.ts       O-01..O-03 (Eva auto-redirect, happy
                                     path create, empty-name submit-disabled)
- tests/e2e/invitation.test.ts       I-01..I-05 (token generation, accept
                                     logged-out + logged-in, expired, used)
- tests/e2e/manage-collective.test.ts MC-02..MC-05 (promote, demote, remove
                                     member, generate pending invite)
- tests/fixtures/db.ts               resetEva, seedExpiredInvitation,
                                     seedUsedInvitation, restoreSeedMembership,
                                     countMembership — all via raw SQL so they
                                     don't depend on SUPABASE_SERVICE_ROLE_KEY

Bugs found & fixed:
- supabase/migrations/012_create_collective_rpc.sql: atomic
  `create_collective(name, emoji)` SECURITY DEFINER RPC. Fase 1 onboarding
  used `.insert(...).select().single()` which triggers the SELECT policy on
  the fresh row — creator isn't a member yet, so the INSERT was rejected with
  "row-level security" even though the INSERT policy passed. onboarding now
  calls the RPC which inserts both the collective AND the creator-as-admin
  membership in one transaction, bypassing RLS. F-08 in rls-isolation.test.ts
  has been silently masking this bug (only the spoof branch was asserted).

- routes/+layout.svelte: post-login redirect now reads sessionStorage
  `pendingInvitationToken` and routes back to /invitation/<token> instead of
  defaulting to /onboarding or /lists. The invitation page already stashed
  the token before kicking off Keycloak, but nothing read it back.

- routes/(app)/collective/manage/+page.svelte: loadMembers now subscribes
  to currentCollective so it re-runs when the store resolves after cold
  navigation. onMount alone races with the auth listener's first emit and
  the member list stayed empty on direct URL hits.

- routes/invitation/[token]/+page.svelte: awaits authLoading before deciding
  whether to redirect to login. Previously raced with the auth listener and
  triggered a redundant Keycloak round-trip for freshly-authenticated users.

Stable selectors added (Playwright):
- Onboarding: onboarding-create-tab, onboarding-join-tab,
  collective-name-input, collective-submit
- Manage: member-row-<uid>, role-select-<uid>, remove-member-<uid>,
  generate-invite, invite-link
- Invitation: invitation-accept, invitation-error (+ data-error-key attr)

Scope dropped from the plan — UI does not exist, would be feature work:
- O-04: "+ new collective" affordance in the sidebar.
- MC-01: rename-collective input in /collective/manage.
Both flagged as follow-ups in plan/fase-7-collective-flow-tests.md.

Test totals: 34 pgTAP + 140 Vitest integration + 15 Vitest unit + 58
Playwright + 1 gated rate-limit. 3 skipped (2 Realtime presence, 1 gated).
2026-04-14 22:33:33 +02:00
d6cea5146f feat(fase-5.10.0): drop shopping_items.unit column
The unit field had no edit path under the redesign (only name + qty
remain in the row; the double-tap overlay is name-only). Users can encode
the unit inside the name when it matters — e.g. "Milk 1L", "Eggs 1 doz".

Changes
  supabase/migrations/011_drop_shopping_items_unit.sql — DROP COLUMN
  supabase/seed.sql — unit values folded into names where relevant
  supabase/tests/002_item_frequency_trigger.sql — remove unit from INSERT
  packages/types — remove unit from ShoppingItem Row/Insert/Update
  apps/web/src/lib/stores/lists.ts — addItem/updateItem signatures
  apps/web/src/routes/(app)/lists/[id]/+page.svelte — purge newUnit /
    editUnit state + inputs + display
  apps/web/src/routes/(app)/lists/[id]/session/+page.svelte — simplify
    quantity-only render
  apps/web/messages/{en,es}.json — remove list_unit_label

Verification
  just test-db  → 34 pgTAP green
  just test-integration → 140 + 2 skipped
  just test-unit → 11 green
  just test-e2e → 39 + 2 skipped

The generated search tsvector on shopping_items.search already indexes
only `name`, so full-text search coverage is unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:00:08 +02:00
2806e06db2 feat(fase-4): global search + RLS isolation audit (211 tests green)
4.0 Tests primero
  Vitest: search.test.ts (10 — S-01..S-04), rls-audit.test.ts (42 —
    3 intruders × 14 cross-collective ops proving zero leakage)
  pgTAP: 007_search_tsvector.sql (9 — columns, GIN indexes, function
    signature, generated-vector invariant)
  Playwright: search.test.ts (2 — SR-01 happy-path, SR-02 filter toggle)

4.1 Búsqueda global
  Migration 010_search_vectors.sql: STORED GENERATED tsvector columns
    on shopping_items/tasks/notes + GIN indexes + search_result_type
    enum + search_in_collective() SECURITY INVOKER function (RLS-aware,
    trash-excluded per RN-08) + GRANT EXECUTE to anon/authenticated.
    Uses `simple` config (no stemmer — bilingual en/es).
  Store apps/web/src/lib/stores/search.ts — RPC wrapper
  /search: debounced input (300ms, ≥2 chars), type filter chips with
    multi-toggle, results grouped per type with data-testid hooks for
    Playwright, deep-link per type (shopping_item → /lists/[id], task →
    /tasks/[id], note → /notes/[id])

4.4 Security
  rls-audit.test.ts replaces the curl-based manual audit with a
    parametrised matrix — zero cross-collective reads/writes under
    three different attacker roles

Realtime flake hardening
  Add 250ms settle after SUBSCRIBED in realtime-helpers.ts — the Phoenix
    socket reports SUBSCRIBED slightly before the backend finishes
    propagating the filter to the WAL subscription, so mutations fired
    immediately after subscribe could miss the filter under load.

4.Z Verification
  just test-all → 211 verdes, 2 skipped
    34 pgTAP (was 25, +9 search)
    140 Vitest integration (was 88, +10 search +42 rls-audit; 2 skipped)
    6 Vitest unit (unchanged)
    31 Playwright (was 29, +2 search)
  Deferred (prod-deploy scope):
    PWA install/push (needs real iOS/Android hardware)
    Kong rate-limit plugin config
    JWT_SECRET / ANON_KEY rotation
    Lighthouse PWA ≥ 90 manual validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:47:38 +02:00
104eeba02e feat(fase-3): Tareas + Notas — TDD complete (148 tests green)
3.0 Tests primero
  pgTAP: 005_tasks_rls.sql (5 — completion CHECK), 006_notes_trash_purge.sql (4 — purge SQL inline + pin/archive CHECK)
  Vitest RLS: rls-tasks.test.ts (14), rls-notes.test.ts (15)
  Playwright: tasks.test.ts (3), notes.test.ts (4)

3.1 Tareas
  Migration 008_tasks.sql: task_lists + tasks + task_list_collective_id() helper
  CHECK constraint: is_completed ⇔ completed_by ⇔ completed_at
  RLS by role + cross-collective isolation
  Store apps/web/src/lib/stores/tasks.ts (optimistic CRUD + reorder)
  /tasks overview (grid + sticky create input + 5s polling)
  /tasks/[id] detail with svelte-dnd-action reorder, flip animation,
    inline edit (dblclick), "Completed by …" attribution via lazy-loaded
    collective_members

3.2 Notas
  Migration 009_notes.sql: notes + note_color enum (8) + pin/archive CHECK
    + 7d trash window in RLS + fn_notes_touch trigger + pg_cron purge job
  Store apps/web/src/lib/stores/notes.ts (CRUD + pin/archive/trash/duplicate)
  /notes board (pinned section testid notes-pinned + 30s polling)
  /notes/[id] editor (title + textarea, 500ms debounced autosave,
    color picker, pin/archive/duplicate/trash actions)
  /notes/archive (restore + send to trash)
  /notes/trash (restore + permanent delete)

3.Z Verification
  just test-all → 148 verdes, 2 skipped:
    25 pgTAP (was 16, +9)
    88 Vitest integration (was 59, +29; 2 presence skipped)
    6 Vitest unit (unchanged)
    29 Playwright (was 22, +3 tasks +4 notes)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:28:19 +02:00
4c913cf495 feat(realtime): Fase 2b.0 infra + first R-series tests green
Enable postgres_changes broadcasts on shopping_items / shopping_lists and prove
the path end-to-end with Vitest Realtime tests and a pgTAP configuration check.

Infra changes that were silently blocking events
- Migration 007: shopping_items + shopping_lists added to supabase_realtime
  publication with REPLICA IDENTITY FULL (UPDATE/DELETE payloads need the full
  row so list_id-based filters work and RLS can evaluate DELETE against OLD)
- Realtime service: DB_USER=supabase_admin (superuser; supabase_replication_admin
  lacks CREATE on the `realtime` schema that the service auto-creates per tenant)
- Realtime service: SELF_HOST_TENANT_NAME=realtime so the seed tenant name
  matches the default supabase-js resolves for localhost URLs (was realtime-dev
  → TenantNotFound)

Tests
- pgTAP 004_realtime_publication.sql — P-01..P-04: publication membership and
  REPLICA IDENTITY FULL for both shopping tables
- Vitest realtime-postgres-changes.test.ts — R-01 INSERT broadcast,
  R-02 UPDATE carries full row, R-03 list_id filter isolates events
- test-utils realtime-helpers.ts — subscribePostgresChanges() returns a
  waitFor(predicate, timeout) helper that captures the SUBSCRIBED handshake
  before returning so mutations issued right after are not lost
- createClientAs now calls realtime.setAuth(token) so the server evaluates
  RLS against the test user's JWT
- Justfile test-db now globs supabase/tests/*.sql so new pgTAP files are picked
  up automatically

Totals: 54→57 Vitest, 12→16 pgTAP, 15 Playwright = 88 tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 02:33:40 +02:00
3af1276c15 feat: Fase 2a — shopping lists CRUD
DB: shopping_lists + shopping_items tables with full RLS, pg_cron jobs
(archive completed lists after 7 days, purge trash after 7 days).
item_frequency table with SECURITY DEFINER trigger that normalises and
upserts on every shopping_items INSERT.

Frontend: /lists overview (featured card + 2-col grid + trash toggle),
/lists/[id] detail (qty stepper, inline edit, swipe-to-reveal-delete,
drag & drop reorder via svelte-dnd-action, ItemSuggestions chips).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 22:25:31 +02:00
7d91705fc2 feat(fase-1): auth, collective management, and DB migrations
- SQL migrations: users, collectives, collective_members, collective_invitations,
  full RLS policies, is_active_member/is_member/is_admin helpers,
  accept_invitation SECURITY DEFINER function, admin auto-promote trigger,
  avatars storage bucket
- Auth refactor: replace keycloak-js with Supabase OAuth (GoTrue OIDC proxy,
  Option B). signInWithOAuth({ provider: 'keycloak' }) + PKCE flow
- GoTrue config: GOTRUE_EXTERNAL_KEYCLOAK_URL + GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI
  added to docker-compose.dev.yml; Keycloak realm updated with GoTrue callback URI
- New routes: /auth/callback, /invitation/[token], /(app)/collective/manage
- Functional onboarding: create collective or join via invite link
- Full settings page: display name (debounced), avatar (initials/emoji/upload),
  language switcher, Keycloak account console link
- Components: Avatar.svelte (initials/emoji/upload with fallback),
  ImageCropper.svelte (cropperjs, 1:1 crop, lazy-loaded)
- i18n: added all Fase 1 message keys (en + es); renamed 'delete' → 'action_delete'
  (JS reserved word); fixed plugin-m-function-matcher major-version URL format
- Database types: manually seeded packages/types/src/database.ts from migrations
- .env.development: committed public dev defaults for SvelteKit type checking
- CLAUDE.md: documented /etc/hosts requirement for keycloak hostname

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 15:29:29 +02:00