Files
Moheeb Zara 32934124c2 mobile: fix flex-row text overflow; parallelize door-ingest poll; docs refresh
Mobile responsiveness: several flex rows had a `truncate` child without
`min-w-0`, so a long value (member/door name, channel name, published form
URL) would not shrink and pushed the row past the viewport on a phone. Added
`min-w-0` (with `flex-1`/`shrink-0` where needed) on the dashboard presence +
door-self-entry rows, the comms channel list, and the form-builder URL.

Door inbound poll: `/api/cron/door-ingest` now polls connections with
Promise.allSettled instead of a sequential loop, so one tick is bounded to the
slowest single poll rather than the sum of all per-call timeouts; each
connection stays isolated and the run is idempotent.

Docs: refreshed ARCHITECTURE (door epic marked complete P1-P5), the README
access-control feature bullet + project-status pointer, a historical banner on
the older AUDIT.md, a door-epic CHANGELOG entry, and the rolling HANDOFF audit
log (passes 65-68). No migration; no behavior change for existing users.
2026-05-20 21:55:49 -07:00

27 KiB

Codebase Audit

Historical snapshot. This is a full-codebase audit as of 2026-05-14, before the certifications, classes, equipment, Stripe-dues, notifications, member-portal, and door (cards / connections / inbound ingest / API-call buttons) work landed. For audit findings, deploy state, and the running session log AFTER this date, see HANDOFF.md (newest entry on top) — it carries the rolling per-pass security audits.

Date: 2026-05-13 through 2026-05-14 (three passes) Scope: every file outside node_modules, .next, and .git. Method: read each source file, cross-check against the live database type definitions in types/database.ts, the canonical schema in scripts/schema.sql, and the column map in DB_SCHEMA_MAP.md.

This document supersedes the older AUDIT.md, IMPLEMENTATION_STATUS.md, and docs/PRODUCTION_AUDIT.md for findings as of this date. Those files remain as historical context.

  • Pass 1 (2026-05-13): fresh-deploy blockers and deployment artifacts.
  • Pass 2 (2026-05-13): validation schema enum drift, RLS privilege escalation, dashboard task filter bug, dead duplicate routes, auth function duplication.
  • Pass 3 (2026-05-14): architecture refactor. Split monolithic lib/actions.ts by domain, introduced lib/permissions.ts and lib/auth-helpers.ts, wired validation into every action with a schema, added 76 real tests, deleted remaining dead code, added scripts/README.md.

All three passes are reflected below.


1. What the system is

A multi-tenant member-management app for hackerspaces. Each tenant is a spaces row; every other table is keyed by space_id. Authentication uses Supabase Auth (email + password, OAuth wiring is stubbed). All data access goes through Supabase with row-level security enforced at the database. The Next.js app uses server components for reads, server actions for writes, and one client-side Realtime subscription for chat.

Stack:

Layer Stack
Framework Next.js 16 (App Router), React 19, TypeScript 5.7
Styling Tailwind v4 (CSS-native config), shadcn/ui, Lucide icons
Backend Next.js server actions, one REST route under /api/paypal/sync, one /api/health route
Data Supabase Postgres + Auth + Realtime
Tests Vitest (unit), Playwright (E2E)
Hosting Vercel, DigitalOcean App Platform, or self-hosted Droplet (Dockerfile + compose ship with the repo)

2. Repo layout

/
  app/
    (app)/                  Protected routes. Layout enforces auth + membership.
      dashboard/            Server component, parallel counts.
      tasks/                Server page + client component.
      projects/             Kanban-style board.
      members/              Admin/board only mutations.
      payments/             Treasurer/admin/board only.
      comms/                Realtime chat (Supabase channel subscription).
      contacts/             Member-writable CRUD.
      ops/                  Knowledge base, secrets, area leads.
      ops/[id]/             KB entry detail + editor.
      ops/new/              KB entry create form.
      import/               CSV import UI (partial).
      settings/             Space + integrations.
    (landing)/              Public landing page.
    auth/callback/route.ts  OAuth code exchange.
    api/
      health/route.ts       Liveness probe (added this audit).
      paypal/sync/route.ts  Per-space PayPal pull.
    login/, signup/         Public auth pages.
    layout.tsx, page.tsx    Root layout, root redirect.
  components/
    ui/                     shadcn primitives.
    app-sidebar.tsx         The actual sidebar (role-gated).
    app-shell.tsx           DEAD: not imported by any page.
    task-claim-button.tsx   Used by tasks-client.
    theme-provider.tsx      next-themes wrapper.
  hooks/
    use-mobile.ts, use-toast.ts
  lib/
    supabase/
      client.ts             Browser client.
      server.ts             Server component client (cookie-bound).
      admin.ts              Service-role client. Bypasses RLS.
      proxy.ts              Middleware session refresher.
    actions.ts              All mutation server actions.
    auth-actions.ts         Auth-specific server actions.
    validations.ts          Zod schemas (most are not wired up, see findings).
    security.ts             Sanitisation, rate limiting, helpers.
    utils.ts                Tailwind classname merge.
    types.ts                Re-exports from generated database types.
  types/
    database.ts             Generated Supabase types. Source of truth for column shape.
  middleware.ts             Auth gate. Public routes list lives here.
  scripts/
    schema.sql              Canonical, idempotent, full-schema deploy.
    014_member_user_id_nullable.sql  Most recent incremental migration.
    patch-dashboard.mjs, fix-actions.js, fix-sidebar.js, fix-sidebar.mjs   DEAD scripts.
  __tests__/                Vitest specs.
  e2e/                      Playwright specs.
  docs/                     Architecture, deployment, database, API, audit.
  Dockerfile, docker-compose.yml, .dockerignore   Self-host artifacts.
  .do/app.yaml              DigitalOcean App Platform spec.
  .env.example              Env var template.
  CLAUDE.md                 Working agreement.

3. Issues fixed in this audit

3.1 space_members.user_id was NOT NULL but the app needs it nullable

The schema declared:

user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE

But addMember() (lib/actions.ts:302) and importMembers() (lib/actions.ts:897) insert rows with no user_id. These are "offline" members added by an admin or CSV-imported, who have not signed up yet. On a fresh Supabase project, both code paths fail with a NOT NULL violation.

Fix applied:

  • scripts/schema.sql: dropped NOT NULL on space_members.user_id. Added a comment explaining the choice.
  • scripts/014_member_user_id_nullable.sql: new incremental migration for existing databases.

The (space_id, user_id) unique constraint still works because Postgres treats NULL as distinct, so multiple offline members with user_id IS NULL coexist. The partial unique index on (space_id, email) (WHERE email IS NOT NULL) prevents duplicate offline-member emails.

3.2 Realtime publication statements were not idempotent

The closing two lines of schema.sql were:

ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_messages;
ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_channels;

ALTER PUBLICATION ... ADD TABLE errors if the table is already in the publication, which breaks the "idempotent" promise the file makes in its header. Wrapped both in DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL; END $$ blocks.

3.3 RLS policies were not idempotent

Section 6 of schema.sql does CREATE POLICY ... without IF NOT EXISTS (which Postgres 15 does not support). Re-running the file on a database that already has policies errored with policy ... already exists. Added a block of DROP POLICY IF EXISTS statements before the CREATE POLICY block.

Net result of 3.1, 3.2, 3.3: scripts/schema.sql now runs cleanly on both a fresh Supabase project and an existing one. The header comment claiming idempotency is now true.

3.4 Missing deployment artifacts

The deployment guide instructed users to "create" a Dockerfile, a docker-compose file, and to manually edit next.config.mjs. None of these artifacts existed in the repo. App Platform users had no spec to reference.

Added:

  • Dockerfile (multi-stage, standalone output, runs as non-root, healthcheck).
  • docker-compose.yml (service definition with healthcheck, env_file).
  • .dockerignore (excludes git, node_modules, dev artifacts, secrets).
  • .do/app.yaml (App Platform spec with health check path).
  • .env.example (every variable the app reads, with comments).
  • app/api/health/route.ts (liveness probe used by Docker, App Platform, and operators).
  • Updated middleware.ts to include /api/health in the public routes list so the healthcheck does not redirect to /login.
  • Updated docs/DEPLOYMENT.md to reference the bundled artifacts instead of asking users to write their own. Also removed the incorrect note about manually editing next.config.mjs (the file already keys output: 'standalone' off DOCKER_BUILD=1, which the Dockerfile sets).

A fresh clone is now: pnpm install, copy .env.example to .env.local, run scripts/schema.sql in Supabase, pnpm dev.

3.5 Dashboard "Quick Chores" filter was wrong

app/(app)/dashboard/page.tsx:49 filtered tasks with .neq('status', 'done') to find open work. The actual task_status enum has both done (legacy) and completed, and completeTask() writes 'completed'. Completed tasks therefore leaked into the dashboard's open-tasks list. Replaced with a positive filter on the open statuses: open, claimed, in_progress, overdue, due_today, blocked.

3.6 Zod validation schemas had wrong enum values

lib/validations.ts declared enums that disagreed with the database in six places. The schemas were mostly unused (only createTask and taskId were wired), so the bugs were latent. Both fixed:

  • createTaskSchema.type: was free-form string, now enum(['task','chore']).
  • createTaskSchema.recurrence: missing biweekly. Added.
  • updateProjectStatusSchema.status: had planning, active, paused, completed, cancelled. Replaced with the real backlog, in_progress, review, done, blocked.
  • addMemberSchema.role and updateMemberSchema.role: missing associate. Added.
  • createContactSchema.contact_type: had vendor, sponsor, partner, city, media, other. Replaced with the real vendor, supplier, partner, landlord, city.
  • createKbEntrySchema.visibility: had public, members, board, admin. Replaced with the real all_members, board, admin_only.

Also tightened: logCashPaymentSchema (removed unused from_identifier, added member_id), updateSpaceSettingsSchema (added slug), upsertAreaLeadSchema (matched the action's parameter shape).

3.7 Privilege escalation on space_members self-update

The members_update RLS policy let a member update their own row including role, tier, status, approved, and has_card_access. An authenticated member could PATCH their own membership via PostgREST and become an admin.

Fixed with a BEFORE UPDATE trigger (prevent_member_self_role_change) that rejects any change to protected columns when the row belongs to the current user and the current user is not already admin/board/treasurer in that space. Service-role calls (no auth.uid()) and privileged self-edits are unaffected. Applied to schema.sql and shipped as incremental migration scripts/015_prevent_member_self_role_change.sql.

3.8 Validation wired into the highest-risk actions

The Zod schemas were imported by lib/actions.ts but never actually invoked except by createTask and claimTask. Wired the now-correct schemas into:

  • createTask (already)
  • claimTask (already)
  • createProject
  • updateProjectStatus
  • addMember
  • logCashPayment
  • linkPaymentToMember

Remaining unwired actions are noted in section 4.

3.9 Duplicate auth functions in lib/actions.ts

Lines 30 to 47 of lib/actions.ts redeclared signIn, signOut, and getUser, each a slightly worse copy of the version in lib/auth-actions.ts. No call site imported them from actions.ts. Removed.

3.10 Dead one-off scripts removed

Deleted scripts/fix-actions.js, scripts/fix-sidebar.js, scripts/fix-sidebar.mjs, and scripts/patch-dashboard.mjs. All four had hardcoded /vercel/share/v0-project paths and were one-time patches that have long since landed in the tree.

3.11 types/database.ts did not match the schema for user_id

After making space_members.user_id nullable in scripts/schema.sql, the generated types were stale. Updated Row.user_id to string | null and Insert.user_id / Update.user_id to optional nullable. Regenerate properly with supabase gen types typescript on next schema sync.

3.12 Signup page copy referenced channels that are not created

The "Create Space" panel told users "Default channels (general, announcements, random) will be created automatically." The schema's create_default_channels trigger creates general, announcements, ops. Updated the copy to match.

3.13 Monolithic lib/actions.ts split by domain

lib/actions.ts was 914 lines and held 29 exported server actions for every entity in the system. Maintenance friction was high. The duplication was severe: 25+ copies of "get user, then look up active member, then check role" and "insert activity_log row" boilerplate.

Refactored into lib/actions/ containing:

  • tasks.ts, projects.ts, members.ts, contacts.ts, payments.ts, knowledge-base.ts, secrets.ts, area-leads.ts, settings.ts, imports.ts — one file per entity, each carrying 'use server'.
  • index.ts — barrel that re-exports every action so the public import path @/lib/actions remains stable.

Every client component that imports from @/lib/actions was verified to keep working. The barrel is plain re-exports; Next.js preserves the 'use server' marking from the source files.

3.14 Shared helpers in lib/auth-helpers.ts and lib/permissions.ts

Two new modules absorb the repeated boilerplate.

lib/permissions.ts:

  • ROLES, ADMIN_ROLES, TREASURER_ROLES, ALL_ROLES, ACTIVE_STATUSES as const tuples.
  • Role and MemberStatus type aliases.
  • hasRole(role, allowed) helper.

lib/auth-helpers.ts:

  • getAuthMember(supabase): returns the active member or null.
  • requireMember(supabase): returns { ok: true, member } | { ok: false, error }.
  • requireMemberWithRole(supabase, allowed, label?): same plus role gate.
  • parseInput(schema, input): Zod parse with a discriminated result.
  • logActivity(supabase, member, action, entityType, entityId, details?): fire-and-forget activity log insert.

The result types use the canonical ok: boolean discriminator so TypeScript narrows reliably under strict: true.

3.15 Validation wired into every action that has a schema

After pass 2, only 7 actions called their schemas. Pass 3 wired the remaining 11:

  • updateMember, approveMember, removeMember
  • createContact, updateContact, deleteContact
  • createKbEntry, updateKbEntry, deleteKbEntry
  • createSecret, deleteSecret
  • upsertAreaLead
  • updateSpaceSettings, saveIntegration
  • disconnectIntegration (inline string check, no Zod schema needed)
  • importMembers, importPaymentsCsv (array-level guard + filter)

Bad input now returns a clean error message instead of a database-level failure.

3.16 Real tests, not assertion theatre

The existing __tests__/actions.test.ts and __tests__/utils.test.ts were inline value assertions that never imported project code. Kept for compatibility, but four new files exercise the real units:

File What it covers
__tests__/validations.test.ts Every Zod schema, asserting real DB enum values pass and legacy ones fail. 37 tests.
__tests__/permissions.test.ts Role constants, ACTIVE_STATUSES, hasRole. 10 tests.
__tests__/security.test.ts sanitizeString, escapeHtml, stripHtml, sanitizeEmail, sanitizeUrl, sanitizeSlug, hasSqlInjectionPatterns, isValidUuid, truncate, checkRateLimit, validateContentLength. 25 tests.
__tests__/auth-helpers.test.ts parseInput parse/error path. 4 tests.

Total: 202 tests across 7 files, all green.

3.17 Vitest config fixed

  • vitest.setup.ts had JSX inside a .ts file (esbuild refused: "Expected '>' but found '{'"). Replaced the next/image mock with React.createElement.
  • vitest.config.ts was picking up Playwright e2e specs and trying to run them. Added explicit include and exclude globs.

3.18 Final dead-code sweep

Deleted:

  • app/auth/login/page.tsx (dead duplicate login at /auth/login linking to non-existent /auth/onboarding).
  • components/app-shell.tsx (no importer; hardcoded badge counts; no role gating).

3.19 Shared types consolidated in lib/types.ts

lib/types.ts already re-exported the generated row and enum types. Added:

  • MemberSummary: common projection used by sidebars and props.
  • ActionResult<T>: standard server-action return shape.
  • Re-exports of Role, ROLES, ADMIN_ROLES, TREASURER_ROLES, ALL_ROLES, ACTIVE_STATUSES, hasRole from lib/permissions.ts.
  • Re-exports of Member, Result, MemberResult, ServerSupabase from lib/auth-helpers.ts.

One-stop import for any consumer of typed entities.

3.20 Migration conventions documented

Added scripts/README.md covering: canonical vs incremental files, naming, the idempotency rules (IF NOT EXISTS, DO ... EXCEPTION, DROP POLICY IF EXISTS, etc), the security baseline for new tables, and verification queries for fresh deploys.


4. Open issues (not fixed in this pass)

These are documented for the next person, not silently shipped. Each one has a concrete location and a concrete fix.

4.1 (was open: dead scripts) RESOLVED in pass 2. See 3.10.

4.2 (was open: dead components/app-shell.tsx) RESOLVED in pass 3. See 3.18.

4.3 (was open: dead app/auth/login) RESOLVED in pass 3. See 3.18.

4.4 (was open: validation not wired everywhere) RESOLVED in pass 3. See 3.15.

4.4a Supabase TypeScript generic drift

@supabase/ssr and @supabase/supabase-js emit SupabaseClient with 3 generics. Our types/database.ts was generated against an older signature. The result: 38 lib/actions/* and 103 app/(app)/* TypeScript errors of the form Argument of type '{...}' is not assignable to parameter of type 'never'.

These errors already existed in the pre-refactor codebase. They are masked by next.config.mjs: typescript.ignoreBuildErrors: true. The runtime is unaffected: Supabase's PostgREST client accepts the values fine.

Fix: regenerate types/database.ts with the latest Supabase CLI (supabase gen types typescript --project-id <ref>). Then drop typescript.ignoreBuildErrors.

4.5 signUp metadata is not consumed by the auth trigger

lib/auth-actions.ts:signUp passes metadata keys full_name, action, space_name, space_slug, city, invite_code. The handle_space_signup trigger in schema.sql reads space_id, role, display_name. Different keys, so the trigger short-circuits and does nothing. That is actually fine because createSpace and joinSpace insert the membership themselves using the admin client. But the trigger is then dead code unless someone passes space_id in data. Either:

  • Document the trigger as a future-use safety net and add a code comment, or
  • Remove the trigger and stop pretending it does anything.

4.6 Several documented features are UI-only or unfinished

Feature Status Location
GitHub / Google OAuth Client wired (signInWithOAuth) but needs provider config in Supabase Auth app/login/page.tsx:31
Webhook endpoint URL displayed in settings, no handler (none)
Zeffy, Venmo, Stripe sync Config storage only, no API code lib/actions.ts:saveIntegration
CSV import UI uploads a file but does not process it app/(app)/import/import-client.tsx
Email notifications None (none)
Task recurrence auto-rollover Cron job not implemented (none)
Secrets encryption Plain text in DB secrets.value

None of these are runtime errors. They are missing-feature stubs. They are listed here so deployers know what is and is not real. Pass 1's audit incorrectly listed OAuth as "UI only, no handlers"; the client-side OAuth call is actually wired. What is missing is the provider configuration in the Supabase dashboard.

4.7 package.json name is "my-project"

Cosmetic. Should be "hackerspace-management". Not a deploy blocker.

4.8 tsconfig.json has "strict": true but next.config.mjs has typescript.ignoreBuildErrors: true

The build does not fail on type errors. This is a v0 default that should be removed once the codebase passes pnpm exec tsc --noEmit cleanly. The pass-2 schema and types edits move the needle. Remaining issues are minor.

4.9 Default channels: 3 vs 4

schema.sql create_default_channels inserts 3 channels: general, announcements, ops. Older docs (DB_SCHEMA_MAP.md, docs/ARCHITECTURE.md) say 4 channels (general, announcements, random, facilities). The schema is current; the older docs are stale. The signup page copy was fixed in pass 2. Update DB_SCHEMA_MAP.md and docs/ARCHITECTURE.md on next docs pass.

4.10 (was open: members RLS privilege escalation) RESOLVED in pass 2. See 3.7.


5. Database column map versus code use

I cross-checked every column accessed in lib/actions.ts, lib/auth-actions.ts, app/api/paypal/sync/route.ts, and the client components named in app/(app)/*. Findings:

  • space_members.last_paid_at: present in schema, used by linkPaymentToMember and importMembers. OK.
  • space_members.last_payment_at: legacy duplicate. Schema keeps both. Code reads either with a fallback (members-client.tsx).
  • knowledge_base.is_pinned vs pinned: schema has both, code uses is_pinned. OK.
  • secrets.title vs label: schema has both, code writes both fields (createSecret at lib/actions.ts:701). OK.
  • payments.status vs link_status: schema has both, code uses link_status. OK.
  • integrations.config vs credentials / settings: schema has all three for legacy compatibility, code uses config. OK.
  • tasks.task_type: column matches enum name. Server actions write task_type, never type. OK.
  • member_status filter in app uses ('current', 'unverified', 'late'). Schema enum matches. OK.

No drift between active code paths and the schema beyond what is in section 4.


6. Security posture

Control State
RLS on every table Yes
Service role key isolated to server Yes, but lib/actions.ts:30-47 and other call sites should be reviewed periodically
Input validation Partial. createSpace sanitises. createTask and claimTask use Zod. The rest do not
Rate limiting Present in lib/security.ts as in-memory map. Wired into signIn and createSpace. Not persistent. For multi-instance deploys, switch to Redis (Upstash)
Secrets encryption at rest None. secrets.value and integrations.config are plain text. Encrypt before merging to production
CSRF / Origin check Implicit through Next server actions + same-origin cookies. No explicit Origin check
Audit log activity_log table is populated by most write actions
Privilege escalation on space_members update Possible per 4.11, fix before production

7. Test posture

  • __tests__/utils.test.ts: assertion-only tests of helpers, no real imports of project code. Useful as smoke tests, not as coverage.
  • __tests__/actions.test.ts: mocked Supabase client, tests the call shape of server actions. Does not exercise RLS, does not hit a database.
  • __tests__/components.test.tsx: not present, but listed in TESTING.md. The doc lies.
  • e2e/auth.spec.ts and e2e/critical-flows.spec.ts: present, Playwright. Not verified to pass.

Recommended next steps: stand up a Supabase local instance for integration tests, add a CI job that runs pnpm test and pnpm test:e2e, fix TESTING.md to reflect reality.


8. Performance notes

  • app/(app)/layout.tsx makes three sequential Supabase calls (member, taskCount, paymentCount). The latter two can run in Promise.all.
  • app/(app)/dashboard/page.tsx runs multiple queries; check whether they parallelise. (Not audited line-by-line, see file.)
  • Realtime: only comms_messages and comms_channels are in the publication. Good.
  • Indexes: 22 indexes are defined in schema.sql, covering every foreign key and common filter (tasks.status, payments.transaction_date, comms_messages.channel_id). Good.

9. Deployment readiness summary

Target Ready? Notes
Local dev Yes pnpm install && pnpm dev, with .env.local filled
Vercel Yes Push to GitHub, connect, set env vars
DigitalOcean App Platform Yes Use .do/app.yaml or the UI flow
DigitalOcean Droplet (Docker) Yes Dockerfile and compose ship with the repo
Database fresh deploy Yes One file: scripts/schema.sql
Database upgrade from existing Yes Run new incremental migrations in scripts/ only

A fresh deploy will succeed. The open issues in section 4 are quality and feature gaps, not deploy blockers.


10. What changed across the three passes

Pass 1 (deployment hardening)

Edited:

  • scripts/schema.sql: user_id nullable, realtime idempotent, RLS policies idempotent.
  • middleware.ts: /api/health added to public routes.
  • docs/DEPLOYMENT.md: env section, App Platform section, Droplet section (no longer asks user to write the Dockerfile/compose), post-deploy checklist (health probe), upgrading section.

Added:

  • CLAUDE.md
  • Dockerfile, .dockerignore, docker-compose.yml
  • .do/app.yaml
  • .env.example
  • app/api/health/route.ts
  • scripts/014_member_user_id_nullable.sql
  • docs/AUDIT.md (this file)
  • docs/HANDOFF.md
  • README.md rewritten

Pass 2 (application repairs)

Edited:

  • app/(app)/dashboard/page.tsx: open-task filter now lists open statuses positively instead of neq('status','done') (was leaking completed tasks).
  • app/signup/page.tsx: default channel list in copy now reads general, announcements, ops to match the trigger.
  • lib/validations.ts: aligned enums for tasks, projects, members, contacts, KB; tightened settings, area-lead, and cash-payment schemas.
  • lib/actions.ts: removed duplicate signIn, signOut, getUser (kept in auth-actions.ts); wired createProject, updateProjectStatus, addMember, logCashPayment, linkPaymentToMember to their Zod schemas.
  • scripts/schema.sql: added prevent_member_self_role_change trigger to stop privilege escalation on self-update.
  • types/database.ts: space_members.user_id now string | null to match the schema.

Added:

  • scripts/015_prevent_member_self_role_change.sql

Removed:

  • scripts/fix-actions.js, scripts/fix-sidebar.js, scripts/fix-sidebar.mjs, scripts/patch-dashboard.mjs

No business rules, no UI styling, no test code were changed. Pass 2 edits are bug fixes, security hardening, schema alignment, and dead-code removal.

Pass 3 (architecture refactor + real tests)

Added:

  • lib/permissions.ts (role constants, hasRole)
  • lib/auth-helpers.ts (requireMember, requireMemberWithRole, parseInput, logActivity)
  • lib/actions/ directory with 11 files (one per domain, plus index.ts barrel)
  • __tests__/validations.test.ts (37 tests)
  • __tests__/permissions.test.ts (10 tests)
  • __tests__/security.test.ts (25 tests)
  • __tests__/auth-helpers.test.ts (4 tests)
  • scripts/README.md

Edited:

  • lib/types.ts (now re-exports helpers + permissions for one-stop import)
  • vitest.config.ts (proper include/exclude globs)
  • vitest.setup.ts (replaced inline JSX in a .ts file with React.createElement)

Removed:

  • lib/actions.ts (replaced by lib/actions/ directory; barrel preserves the public import path)
  • app/auth/login/page.tsx (dead duplicate)
  • components/app-shell.tsx (unused, hardcoded badges)

Test suite: 202 tests, 7 files, all passing.

Pass 3 changes UI nothing, schema nothing, business rules nothing. They are pure organisation, test coverage, and dead-code removal.