# Plan 001: Require auth + role/scope checks on all public Convex handlers

> **Executor instructions**: Follow step by step. Run every verification command
> and confirm the expected result before the next step. If a STOP condition
> fires, stop and report — do not improvise. Update `plans/README.md` status
> when done (unless a reviewer maintains the index).
>
> **Drift check (run first)**:
> `git diff --stat 74051ba..HEAD -- packages/backend/convex packages/shared/src/domain.ts apps/web/src/features/shell`
> If in-scope files changed, re-read excerpts below against live code before proceeding.

## Status

- **Priority**: P0
- **Effort**: L
- **Risk**: MED
- **Depends on**: none
- **Category**: security
- **Planned at**: commit `74051ba`, 2026-07-09

## Why this matters

Convex public mutations/queries currently apply domain writes and return full
tables without requiring a session or checking role/ownership. Comments in code
explicitly say master-only gating is client-side only. With Better Auth live
and the prototype god-mode bar retired (ADR-0005), this is production-grade
account-takeover / multi-tenant dump risk: `resetPassword`, `inviteInternal`,
and `deliveries.list` are the sharpest edges.

## Current state

- `packages/backend/convex/users.ts` — `setClientRole` (~112), `inviteInternal`,
  `updateInternal`, `deactivate`/`reactivate`, `resetPassword` (~472) have no
  caller role check. Comment at ~466: “Gating master-only só no client”.
- `packages/backend/convex/deliveries.ts` — `list` (~213) does
  `ctx.db.query("deliveries").collect()`; `accept` (~258) patches status with
  no auth; `submitRequest` falls back to `PORTAL_COMPANY_ID` when unauthenticated
  (~697–701).
- `packages/backend/convex/companies.ts` — `create` / `saveSetup` open.
- `getAppUserId` / `actorFor` used mainly for audit labels, not denial.
- Domain helpers for gating already exist in `packages/shared/src/domain.ts`
  (`isOwner`, `canSeeUsuarios`, `scopedDeliveries`, `wouldDisableLastActiveMaster`, …).
- Client UX guards: `apps/web/src/features/shell/role-guard.tsx` — not a server boundary.

### Vocabulary (CONTEXT.md)

- **Face**: `portal` | `backoffice`
- **BO roles**: `master` | `gerente` | `analista`
- **Portal roles**: `editor` | `leitor`
- Status do interno é **derivado** (`effectiveStatus`) — não inventar campo tri-state.

### Conventions

- Funções com **objeto como parâmetro** (nunca posicional).
- pt-BR em erros de produto e comentários.
- Domínio canônico em `@dudata/shared`; backend já importa shared.

## Commands you will need

| Purpose | Command | Expected on success |
|---------|---------|---------------------|
| Typecheck backend | `cd packages/backend && bun run typecheck` | exit 0 |
| Typecheck monorepo | `bun run typecheck` | exit 0 |
| Tests monorepo | `bun run test` | all pass |
| Convex dev (manual) | `bun run dev` (or filter backend) | functions deploy to dev |

## Steps

### 1. Add shared authz helpers in backend

Create `packages/backend/convex/lib/authz.ts` (or equivalent next to existing auth helpers) with:

- `requireAppUser({ ctx })` → domain user doc or throw `ConvexError("Não autenticado")`
- Reject when `active === false` (deactivated)
- `requireBoRole({ user, roles: BoRole[] })`
- `requireMaster({ user })`
- `requirePortalCompany({ user, companyId })` — portal users only their `companyId`
- Reuse `getAppUserId` from auth module; do not invent a second session source

**Verify**: typecheck backend passes; helpers exported and importable.

### 2. Lock high-value actions first

Wire authz on (in this order):

1. `users.resetPassword` — master only
2. `users.inviteInternal` — master only
3. `users.deactivate` / `reactivate` / `updateInternal` — master (+ last-master / self-lockout using domain predicates)
4. `companies.create` / `saveSetup` — master (saveSetup: owner/gerente scope per product rules in domain)

**Verify**: unauthenticated call throws; non-master throws. Prefer `convex-test` if plan 003 lands same PR; otherwise a small internal unit test of helpers + manual Convex dashboard check.

### 3. Lock delivery mutations

Every public delivery mutation: require auth; enforce face/role/assignee/owner using the same rules as `deliveryActions` in shared domain (see plan 002 for status preconditions — apply role matrix here even if status assert lands in 002).

`submitRequest`: **remove** unauthenticated `PORTAL_COMPANY_ID` fallback; require session; portal forces `user.companyId`; BO may pass `companyId` only if master/gerente allowed.

### 4. Scope all list/get queries

- `deliveries.list` → replace or add scoped variants: portal by `companyId`; BO via `scopedDeliveries` logic server-side (or role filters). Never return other tenants to portal.
- Same for `users.list`, `users.listClients`, `companies.list`, `activityLogs`, `auditLogs`, `contabilidadeSocial`.

Update web/mobile call sites if query signatures change. Prefer additive new queries + migrate screens, then delete open `list` if unused.

### 5. Deactivate must revoke sessions

In `deactivate`, after `active: false`, call the same session-kill path used by password reset (`deleteUserSessions` / adapter). Ensure `requireAppUser` rejects inactive.

### 6. Audit

Keep `logAudit` with **server** actor only; never trust client-supplied actor.

## Out of scope

- Mobile UI redesign
- Portal client-user management UI (DIR-02)
- Pagination / PERF-01 (can pair later but not required for authz)
- Changing Better Auth providers

## STOP conditions

- If product owner says portal multi-tenant rules differ from `scopedDeliveries` / CONTEXT.md — stop and ask; do not invent RBAC.
- If a public query is required for unauthenticated LP/health — confirm it is not domain data (health stays open).
- If `getAppUserId` cannot resolve domain user for seeded accounts — stop; fix link `authId` first.

## Done criteria

- [ ] Every public mutation/action in `users.ts`, `companies.ts`, `deliveries.ts` calls a require* helper that can deny.
- [ ] `resetPassword` and `inviteInternal` reject non-master and unauthenticated.
- [ ] No public query returns full multi-tenant dump without auth + scope.
- [ ] `submitRequest` without session fails; no write to default company.
- [ ] `bun run typecheck` and `bun run test` pass.
- [ ] Deactivated user cannot keep using API with old session (or session deleted).

## Test plan

- Unit: authz helpers (master/gerente/analista/portal/inactive).
- Integration (prefer plan 003 harness): invite as analista fails; list as portal only own company; accept as wrong role fails.

## Maintenance

Future mutations must import `requireAppUser` — document in root AGENTS.md (plan 005). Code review checklist: “public handler has authz?”.
