347 Commits
Author SHA1 Message Date
Superminaren d81a49175b Updated
CI / check (push) Canceled after 0s
CI / integration-e2e (push) Canceled after 0s
Deploy to production / deploy (push) Canceled after 0s
2026-10-14 23:30:02 +02:00
Moheeb Zara 04145e5985 Merge #9: single-tenant mode + setup CLI + drop unsafe signup trigger
CI / check (push) Canceled after 0s
CI / integration-e2e (push) Canceled after 0s
Deploy to production / deploy (push) Canceled after 0s
Single-tenant deployment mode + setup CLI (+ dropped unsafe signup trigger)
2026-08-05 13:20:36 -07:00
Moheeb Zara c0d5ffd8fc audit: implement --force admin password reset + update HANDOFF
Quick audit before merge found the docs promise 'pnpm setup create-admin --force'
resets an existing admin's password, but the CLI only reused the user without
resetting (an admin-lockout footgun). Implement it via auth.admin.updateUserById,
guarded by --force and only when a password is supplied. Update docs/HANDOFF.md
with the single-tenant + security-fix entry (newest on top).
2026-08-05 13:15:27 -07:00
Moheeb Zara 7b8d2ae167 feat(tenant): setup CLI, CD glue, bundled compose, docs + security fix
Tier 2 productization for single-tenant instances, plus fixes from an
adversarial review pass.

New:
- scripts/setup.mjs: 'pnpm setup' CLI (doctor/provision/create-admin/all),
  idempotent, creates the first admin via the Supabase Admin API (email_confirm),
  flag > env > .env.local precedence, masked interactive prompts, --yes/--force.
- scripts/apply-migrations.sh + deploy/deploy.sh: reproducible, idempotent CD
  glue (previously only on the prod droplet), tracked via public._migrations_applied.
- docker-compose.full.yml + .env.full.example: one-command app + self-hosted
  Supabase bundle (compose config validated).
- docs/SINGLE_TENANT.md (deploy + CLI guide) and docs/CUSTOMIZE.md (the
  documented lib/tenant.ts config pattern for white-labeling); README pointer.
- __tests__/tenant.test.ts: 19 unit tests for resolveTenantConfig + isMarketingPath.

Security (found by review, pre-existing):
- Drop the stale handle_space_signup() auth trigger, which derived member role
  and space from client-controlled raw_user_meta_data and could self-insert an
  admin. Membership is created only by the vetted server actions.
  Migration scripts/056_drop_stale_signup_trigger.sql + folded into schema.sql.

Fixes:
- setup.mjs masked-password prompt hung (callback passed to readline/promises
  question); use the awaited form.
- notifications-enqueue.test: base-URL default is now localhost (platform-domain
  fallback removed), not hackerspace.sh.
- docs accuracy (logo/BrandMark, apply-migrations does not restart PostgREST, --yes).

Gates: tsc 0, lint 0 errors, 664 unit tests, build ok.
2026-08-05 13:02:18 -07:00
Moheeb Zara 646d2e542c feat(tenant): single-tenant mode core (config SSOT + server/UI guards + branding)
Add lib/tenant.ts as the single source of truth for multi- vs single-tenant
deployment. Gate space creation (server + signup UI), add open-join to the one
space, hide the marketing shell, and route brand name + base URL through config.
Also commit the _migrations_applied table into the canonical schema so a fresh
instance is reproducible from schema.sql alone.

- lib/tenant.ts: resolveTenantConfig (pure) + tenantConfig() + appBaseUrl()
- lib/auth-actions.ts: createSpace refused when !allowSpaceCreation; joinSpace
  open-join resolves the configured/sole space with no invite code
- proxy.ts: marketing paths redirect to /login when showMarketing is off
- app/signup/page.tsx: single-tenant hides create/join chooser, brand=siteName
- app/layout.tsx + 4 link builders: brand/base-URL via config (no hackerspace.sh literal)
- scripts/schema.sql: public._migrations_applied tracking table
- .env.example: documented single-tenant + first-admin bootstrap vars
- package.json: pnpm setup
2026-08-05 12:43:11 -07:00
Moheeb Zara 85f3655e6f docs: update HANDOFF with shipped docs state + the proxy public-route fix
Records that the /docs section and homepage fix are merged (PR #8) and live,
the proxy.ts PUBLIC_ROUTES hotfix that made /docs reachable without a session,
and the authoring/middleware gotchas for future public route groups.
2026-08-05 11:39:46 -07:00
Moheeb Zara 877f787383 fix(proxy): make /docs public so the docs site is reachable without a session
The new /docs section is static, public help content, but proxy.ts (the auth
middleware) redirects any non-allowlisted path to /login, so unauthenticated
visitors were bounced off /docs to the login page. Add /docs to PUBLIC_ROUTES
(covers /docs and every /docs/<category>/<slug>); screenshots under /docs-media
already bypass the middleware via the image matcher.

Verified locally with no session: /docs and doc pages return 200, while
/dashboard and /members still redirect to /login.
2026-08-05 03:26:26 -07:00
Moheeb Zara 08290302a5 Merge #8: public docs section (Diátaxis) + homepage fix for signed-in users
34-page /docs section (audited + screenshotted, no em-dashes) and the landing auth-aware CTA fix. See docs/HANDOFF-audit-cleanup.md.
2026-08-05 03:20:11 -07:00
Moheeb Zara 8edc9da611 docs: audit corrections, coverage pages, screenshots, and em-dash removal
Finalizes the docs section after an adversarial audit (code review + per-page
fact-check + coherence critic) and two content requests.

- Accuracy: fixed 10 invented cross-link slugs, the admin-vs-board gating claims
  the verifiers caught (permissions matrix, /settings, invites, onboarding,
  connect-payments, proposal withdraw, Stripe reconciliation, member fields,
  door.operate), and 2 issues in the new pages (project area-at-creation, forum
  thread-delete role).
- Coverage: added 5 how-to pages so every module is documented (recurring Stripe
  dues, tasks & chores, projects, comms & forum, ops knowledge base + secrets
  vault). 34 pages total.
- Screenshots: 12 captioned app screenshots under public/docs-media/, embedded
  as figures. Each tutorial now has 2-3 step screenshots.
- Style: removed all em-dashes/en-dashes/ellipses from the docs (natural ASCII
  punctuation).
- Handoff (docs/HANDOFF-audit-cleanup.md) updated with the current state.

typecheck 0 · lint 0 errors · build ok (all 34 /docs pages prerendered)
2026-08-05 03:15:07 -07:00
Moheeb Zara 16229ae523 feat(docs): add a Diátaxis documentation section to the public site
A new public /docs area covering the whole platform, organized by Diátaxis:
tutorials (learning), how-to guides (tasks), reference (facts), and explanation
(the why). 29 pages, each grounded in the actual routes, actions, schema, and
UI — verified against the code and corrected for accuracy (e.g. admin-only vs
admin+board gating, where the permissions matrix actually lives, how Stripe
reconciliation matches).

Implementation:
- lib/docs/nav.ts is the single source of truth (categories + pages), driving
  the sidebar, static params, and prev/next.
- Content authored as content/docs/<category>/<slug>.md, read at build time
  (the routes are statically generated), rendered by <DocsMarkdown> — reuses the
  existing react-markdown/GFM stack, no new dependencies.
- (docs) route group with its own theme (docs.css) matching the public site's
  dark / monospace / lime aesthetic; sidebar with active-state, breadcrumb,
  prev/next, and a Diátaxis home. Public — signed-in users can read it too.
- "Docs" link added to the landing nav.

typecheck 0 · lint 0 errors · build ok (all /docs pages prerendered)
2026-08-05 00:42:42 -07:00
Moheeb Zara fc6adaf16e fix(landing): let signed-in users browse the marketing site instead of bouncing to /dashboard
The homepage hard-redirected any authenticated visitor straight to /dashboard,
so clicking the wordmark/home (or opening / while signed in) never showed the
public site — and would block the public docs section too. Drop the redirect and
make the nav, hero, and closing CTAs auth-aware: signed-in visitors get a
"Dashboard" button; signed-out visitors get "Log in" / "Get started".
2026-08-05 00:02:35 -07:00
Moheeb Zara 1b0794d8ff Merge #7: audit, cleanup & architecture refactor — type-safety, bug fixes, decomposition, DB-backed CI
45-commit audit/cleanup/refactor + SoC pass, plus follow-ups: type regeneration, L3 onboarding-race atomic RPC (migration 055), and DB-backed integration+e2e CI. All gates green. See docs/HANDOFF-audit-cleanup.md.
2026-08-04 23:48:21 -07:00
Moheeb Zara 7a5d99cf01 test(e2e): tolerate the /login redirect on protected-route load checks
The "should load X page" specs asserted the URL stays on the protected route,
but an unauthenticated run — as in CI, which starts a fresh dev server with no
session — correctly redirects to /login. They passed locally only by racing an
already-warm reused server. Accept either the target route or /login, matching
the tolerance the sibling assertions in this file already use.

Confirmed with the full e2e suite (60 tests, chromium) under CI conditions
(fresh server, single worker, JWT keys).
2026-08-04 23:43:28 -07:00
Moheeb Zara 5047aef0da ci: run DB-backed integration + Playwright e2e with a Supabase service
Adds an integration-e2e job to ci.yml alongside the fast hermetic check job. It
brings up the Supabase CLI stack, applies the canonical scripts/schema.sql +
seed.sql, then runs the integration suite (psql-driven shipped-SQL: RPCs / RLS /
triggers / constraints) and the Playwright e2e suite (chromium) against the app
wired to that stack (env sourced from `supabase status`).

The integration suite self-skips when no DB is reachable, so a missing DB could
pass silently. Two guards prevent that: an explicit `select 1` reachability
check, and a post-run assertion that fails the job when the suite executed 0
tests (parsed from the vitest json reporter).

Auto-seed is disabled for the ephemeral CI checkout (seed.sql needs tables that
schema.sql creates outside the migration path); schema + seed are applied
explicitly. Local dev is unaffected.

Validated locally: integration suite (58 tests) and both e2e specs (53 tests,
chromium) pass against the real local stack.
2026-08-04 22:53:48 -07:00
Moheeb Zara 4aa6646499 fix(onboarding): atomic DB append for step completion (deferred bug L3)
markOnboardingStepDone read space_members.onboarding_progress, appended the step
id in JS, and wrote the whole jsonb object back — a lost-update race where two
near-simultaneous completions (double-submit, or two steps ticked quickly) both
read the same base array and the second write clobbered the first, silently
dropping a step.

Replace it with mark_onboarding_step_done (migration 055): a single atomic
UPDATE that dedup-appends the step id into onboarding_progress.completed_step_ids
under the row lock — no update can be lost (READ COMMITTED re-evaluates the jsonb
expression against the latest tuple via EvalPlanQual), other keys are preserved,
and re-appending an existing id is a no-op. Ownership-guarded by auth.uid() so a
caller can only complete steps on their own membership row; the no-JWT service
path is trusted and scoped by p_member_id (mirrors class_signup_tx et al.).

integration/onboarding-progress.test.ts covers dedup/idempotency, key
preservation, the auth.uid ownership gate, and the point of the fix: 12
concurrent completions all survive.

Migration 055 folded into schema.sql; types/database.ts regenerated.
typecheck 0 · lint 0 errors · 645 unit · 58 integration · build ok
2026-08-04 22:36:12 -07:00
Moheeb Zara b2b463491a refactor(types): regenerate database types from canonical schema; drop TODO(types) join casts
Regenerate types/database.ts from a local Supabase built from the canonical
scripts/schema.sql. The running dev DB was stale (missing the 'stripe'
payment_platform value, members_with_permission, spaces.host_requires_card,
payments.link_status, dues_payment_methods.space_id), so it was rebuilt from
schema.sql first.

With real relationship metadata the 11 `as unknown as … // TODO(types)` join
casts are no longer needed:
- me portal, session-attendance, member-certifications, equipment-reservations:
  narrow the action result with `'data' in res` instead of a blanket cast.
- stripe / door embeds read the now-typed relation directly.
- completeSession + listEquipmentReservations route the embedded-select query
  through the fully-typed createClient() rather than the wide ServerSupabase gate
  client (the pattern documented in auth-helpers.ts); equipment disambiguates its
  two space_members FKs with an explicit !fkey hint.

Collapse the hand-maintained governance rows/enums in types/domain/governance.ts
into Tables<>/Enums<> aliases.

Tighten duesPaymentMethodSchema.platform to the DUES_LINK_PLATFORMS literal union
(the old `as unknown as [string,...]` erased it), which the now-correct
payment_platform enum requires.

The hand-added class_signup_tx / class_cancel_tx RPC entries are now generated
natively.

typecheck 0 · lint 0 errors · 645 tests · build ok
2026-08-04 22:21:29 -07:00
Moheeb ZaraandClaude Opus 4.8 4f4e3096ca docs: add HANDOFF-audit-cleanup.md (state, verify steps, follow-ups, merge path)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 21:27:26 -07:00
Moheeb ZaraandClaude Opus 4.8 2226f8f746 polish(ops): friendly not-found message on cross-space area-lead update
updateAreaLead used .single(), which errors (raw PostgREST message) rather than
returning null when the id isn't in the caller's space. Switch to .maybeSingle()
so the existing 'Area lead not found' guard is reached, matching deleteAreaLead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 21:26:55 -07:00
Moheeb ZaraandClaude Opus 4.8 8ddc8b7b88 docs: record the SoC pass + browser smoke test; update known limitations
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 19:12:28 -07:00
Moheeb ZaraandClaude Opus 4.8 61ffc2e01f fix(invites): distinguish CAS contention from a real use-cap (retry vs reject)
claimInviteUse returned null both when the cap was truly reached and when it
lost the compare-and-swap 5× under load, so a legitimate joiner on a busy
high-cap invite got a false 'invite has reached its use cap'. Return a
discriminated result; map contention to a distinct 'try again' message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 19:10:52 -07:00
Moheeb ZaraandClaude Opus 4.8 33b2475e67 fix: revalidate /customize (not /settings) after customize-panel mutations
The invites/onboarding/areas/roles/tiers admin panels moved to /customize, but
their actions still revalidated the old /settings route, so the /customize
Router Cache went stale after a write. Point them at /customize (keeping the
other cross-page revalidations).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 19:10:52 -07:00
Moheeb ZaraandClaude Opus 4.8 2dbd52e2e4 test: add authz/scope/anti-impersonation tests for the new actions
Real mock-client tests for createAreaLead/updateAreaLead/deleteAreaLead (admin
gate + space scope), sendMessage (identity from session not input; foreign
channel rejected), and payments member-in-space rejection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 19:10:42 -07:00
Moheeb ZaraandClaude Opus 4.8 3243e4ce98 fix(payments): verify member_id belongs to caller's space (IDOR)
logCashPayment and linkPaymentToMember accepted a uuid member_id without
checking it belongs to the caller's space (the equipment/class paths were
already hardened for exactly this). Look up the member scoped to the caller's
space and reject a foreign id before writing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 19:10:42 -07:00
Moheeb ZaraandClaude Opus 4.8 1aa0cd818a fix(comms): send messages via a server action, derive identity server-side (SoC)
The chat composer inserted comms_messages straight from the browser with
CLIENT-SUPPLIED user_id/display_name/handle/space_id — a member could forge
another person's name/handle. Add a sendMessage server action that derives
identity from the session, verifies the channel belongs to the caller's space,
validates content, and logs the send. The optimistic insert + realtime
subscription are preserved, so latency is unchanged. loadMessages stays on the
browser client (the realtime subscription needs it) with a comment noting it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 19:10:42 -07:00
Moheeb ZaraandClaude Opus 4.8 510187a58d fix(ops): route area-lead roster writes through server actions (SoC)
The ops UI created/updated/deleted area_leads directly from the browser (the
delete had no admin gate or space scope). Add createAreaLead/updateAreaLead/
deleteAreaLead (admin-gated, space-scoped, validated, audited) and wire the
modal + delete handler to them, dropping the direct browser writes. The
distinct area_code-keyed upsertAreaLead interface is left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 19:10:42 -07:00
Moheeb ZaraandClaude Opus 4.8 46ea2f1cce docs: add ARCHITECTURE.md; mark Phase C+D complete in the audit log (Phase D)
ARCHITECTURE.md documents the layered structure, the server-action contract +
space-scope invariant, feature-folder conventions, and an add-a-feature guide.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 06:20:38 -07:00
Moheeb ZaraandClaude Opus 4.8 f775f954ad refactor: dead-code cleanup + drop the lib/types shim (slice 9)
- Delete orphaned components (theme-provider, task-claim-button), the unused
  roleDisplayName helper, the fully-dead cleanupRateLimits, the 6 never-imported
  validation schemas (signIn/signUp/createSpace/joinSpace — a replaced auth flow;
  proposalStatuses; updateAreaLeadRoleSchema).
- Migrate all @/lib/types importers to the canonical @/types/domain and delete
  the compatibility shim.

tsc 0, 635 tests green, lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 06:19:11 -07:00
Moheeb ZaraandClaude Opus 4.8 71ba558522 fix(secrets): store ops-created secrets encrypted via createSecret (security)
The ops 'Add Secret' modal wrote the secret value straight to the DB from the
browser in PLAINTEXT — despite the 'Stored encrypted at rest' label — because
the AES key is server-only. Route it through the createSecret server action,
which encrypts the value (AES-256-GCM), enforces the admin/board gate, validates
input, and logs the activity. Existing plaintext rows (encryption_version 0)
still reveal correctly; new secrets are encrypted when a key is configured.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 06:15:45 -07:00
Moheeb ZaraandClaude Opus 4.8 9100532bef refactor(members): decompose members-client + fix form-field focus bug (slice 8)
663-line members-client.tsx → a 255-line orchestrator + members/components/
{member-form-dialog,members-table,members-toolbar}.tsx + types.ts. The add/edit
form fields (previously defined INSIDE render, which remounted and lost input
focus every keystroke) are now a stable top-level component in member-form-dialog
— fixing that footgun. tsc 0, build ok.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 06:14:25 -07:00
Moheeb ZaraandClaude Opus 4.8 543b1d65eb refactor(settings): decompose settings-client into panels (slice 7)
663-line settings-client.tsx → a 79-line orchestrator (activeTab + shared save
status) + settings/panels/{space,integrations,dues,webhooks}-panel.tsx each
owning its own form state + types.ts. Server-action calls and fields preserved
verbatim. tsc 0, build ok.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 06:14:24 -07:00
Moheeb ZaraandClaude Opus 4.8 7bc6609ac2 refactor(ops): decompose ops-client into panels + components (slice 6)
956-line ops-client.tsx → a 230-line orchestrator + ops/panels/{kb,secrets,
area-leads}-panel.tsx (kb+processes share kb-panel via a variant prop) +
ops/components/{kb-modal,secret-modal,area-lead-modal,secret-row,kb-entry-row}.tsx
+ types.ts, following the customize/ exemplar. Behavior preserved (direct writes
kept as-is for now — the secrets one is fixed in the next commit). tsc 0, build ok.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 06:14:24 -07:00
Moheeb ZaraandClaude Opus 4.8 f86fff62d0 refactor(actions): split forms.ts into a folder (slice 5)
805-line lib/actions/forms.ts → lib/actions/forms/{manage,submit,member}.ts
('use server') + _guard.ts (requireFormsManager/isUniqueViolation) + index.ts
barrel. All 15 actions preserved verbatim (incl. linkSubmissionsByEmail, still
imported by members.ts). tsc 0, tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 06:01:02 -07:00
Moheeb ZaraandClaude Opus 4.8 39bcdd9677 refactor(actions): split door.ts into a folder (slice 4)
813-line lib/actions/door.ts → lib/actions/door/{connections,operate,self-entry}.ts
('use server') + _guards.ts + _audit.ts + index.ts barrel. All 14 actions
preserved verbatim; lib/door/ (executor/ingest/secret) untouched. tsc 0, tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 06:01:02 -07:00
Moheeb ZaraandClaude Opus 4.8 dbaae4851b refactor(actions): split classes.ts into a folder (slice 3)
908-line lib/actions/classes.ts → lib/actions/classes/{definitions,sessions,
signups,instructor}.ts ('use server') + _guard.ts (requirePermission/Gate) +
_forms.ts (findSpaceForm/hasFormSubmission) + a plain index.ts barrel. All 14
actions preserved verbatim; lib/actions/index.ts unchanged. Helpers stay
internal (not re-exported). tsc 0, tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 06:01:02 -07:00
Moheeb ZaraandClaude Opus 4.8 e205ce54c0 docs: record CI + Phase C slices 0-2 in the audit log
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-02 04:22:32 -07:00
Moheeb ZaraandClaude Opus 4.8 3f7e9d96fa refactor(types): move hand-written domain types to types/domain/* (slice 2)
lib/types.ts (aliases + governance shapes + auth re-exports) split into
types/domain/{index,governance,reexports}.ts, making the generated/hand-written
seam explicit (generated = types/database.ts, hand-written = types/domain/*).
lib/types.ts is now a re-export shim so all 11 importers are untouched. Fixed
the stale 'not yet in types/database.ts' comment — those tables/enums are now
generated; de-duplicating the governance rows into Tables<>/Enums<> is a noted
follow-up. tsc 0, 635 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-02 04:21:19 -07:00
Moheeb ZaraandClaude Opus 4.8 18d5e22ad4 refactor(validations): split lib/validations.ts into a barrel'd folder (slice 1)
1141-line lib/validations.ts → lib/validations/<feature>.ts + primitives.ts +
an index.ts barrel. '@/lib/validations' now resolves to the barrel, so all 40
importers are untouched. Every one of the 143 exports is preserved verbatim
(verified by symbol-diff); schemas moved unchanged. tsc 0, 635 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-02 04:19:11 -07:00
Moheeb ZaraandClaude Opus 4.8 c8c60358c7 refactor(types): mark types/database.ts as generated (slice 0)
Banner documenting the regenerate command and that hand-written domain types
belong in types/domain/*, not here. No code change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-02 04:11:25 -07:00
Moheeb ZaraandClaude Opus 4.8 f571001ac2 ci: add a checks-only workflow (typecheck + lint + unit tests)
Runs on every PR and on main after merge, fully separate from deploy.yml so it
can never affect deploys. Catches type/lint/test regressions that previously
had no CI at all. Integration + e2e (which need a DB service) are a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-02 04:10:57 -07:00
Moheeb ZaraandClaude Opus 4.8 edffb2780c docs: record Phase A + Phase B progress in the audit log
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 21:33:40 -07:00
Moheeb ZaraandClaude Opus 4.8 ad010edb41 test: cover the authz primitives (getAuthMember/requireMember/requireMemberWithRole)
These gate every server action but had no direct tests. Add cases for: no user,
no active membership, active member; role allow/deny; the require_approval
unverified gate; and late members keeping their role.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 21:33:15 -07:00
Moheeb ZaraandClaude Opus 4.8 9bd21b185d fix(import): guard unparseable CSV dates and never wedge the button (L7)
A non-empty invalid transaction_date ('N/A') passed presence-only validation,
then new Date(str).toISOString() threw mid-import: nothing imported, no toast,
and setImporting(false) never ran, sticking the button on 'Importing...'.
Extracted parsePaymentRow to lib/import-logic.ts with a date guard, and wrapped
runImport so the importing flag always clears.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 21:31:51 -07:00
Moheeb ZaraandClaude Opus 4.8 8274af7d5c fix(equipment): verify booked-for member belongs to caller's space (L1)
reserveEquipment used a manager-supplied memberId directly, so a booking could
be stamped for a member id from another space. Mirror signUpForClass: look up
the target by (id, space_id=caller) and reject if absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 21:31:51 -07:00
Moheeb ZaraandClaude Opus 4.8 c7a6cd3853 fix(proposals): block unverified members from governance writes (L2)
createProposal/openProposal/withdrawProposal/castVote gated only with
requireMember, which admits status='unverified' — so a member pending approval
in a require_approval space could propose and cast counted votes before vetting.
Add the isPrivilegeEligible gate (same as requireMemberWithRole) to all four.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 21:31:51 -07:00
Moheeb ZaraandClaude Opus 4.8 7fc08c5165 fix(incidents): restrict appealIncident to admin/board, no orphan proposal (M2)
appealIncident was gated by requireMember but the incident UPDATE is RLS-
restricted to admin/board, so a reporter's appeal inserted a draft proposal,
updated 0 incident rows, and returned success — an orphan proposal. Gate with
ADMIN_ROLES to match the write, and assert the link UPDATE affected a row
(.select), rolling back the draft proposal if it did not.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 21:31:51 -07:00
Moheeb ZaraandClaude Opus 4.8 7fe3ffc632 fix(invites): block re-arming an admin invite without role change (M1)
updateInvite ran the canAssignInviteRole guard only when the payload included
'role', so a board member could re-enable/extend an exhausted admin-granting
invite (is_enabled/max_uses/expires_at) and distribute admin access they cannot
mint. Load the existing invite and require canAssignInviteRole against its
effective role whenever any privilege-affecting field is (re-)armed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 21:31:51 -07:00
Moheeb ZaraandClaude Opus 4.8 4b98c21e2b fix(payments): advance-only last_paid_at, no dues regression (M4/M5)
logCashPayment and linkPaymentToMember unconditionally set last_paid_at to the
(possibly backdated) transaction_date and forced payment_status='current', so
logging/linking a historical payment moved a member's dues state backward.
New pure resolveDuesAdvance() keeps the later of existing/incoming date and only
marks 'current' when the payment actually advances; both sites read the current
date first. Identical in the common (newest-payment) case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 21:31:34 -07:00
Moheeb ZaraandClaude Opus 4.8 90c68faa25 fix(door): correct parseHeatSyncLog stale time + token reuse (L5)
(a) Reset h/mi/e after each event so a later event with no H:M:E of its own no
longer inherits the previous event's time-of-day (fabricated audit timestamp +
dedupeKey). (b) Track consumed high-half token indices so one g/d token can no
longer be paired with two adjacent grants (wrong reconstructed card numbers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 21:31:34 -07:00
Moheeb ZaraandClaude Opus 4.8 a3e9b97292 fix(door): block hex IPv4-mapped IPv6 in the SSRF filter (M3)
isBlockedDoorIp blocked the dotted IPv4-mapped form (::ffff:169.254.169.254)
but the hex form ::ffff:a9fe:a9fe (=169.254.169.254 IMDS) / ::ffff:7f00:1
(=127.0.0.1) fell through the IPv6 branch and returned false. Decode the hex
IPv4-mapped form to dotted IPv4 and run the existing blocked-range checks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 21:31:34 -07:00
Moheeb ZaraandClaude Opus 4.8 f337a85682 build: remove typescript.ignoreBuildErrors now that tsc is clean
tsc --noEmit reports 0 errors (was 586, all masked). next build now
type-checks on every build and prod deploy, so a type regression fails CI
instead of shipping. 649 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 21:23:22 -07:00