Policy header, all five child collections, and the insurance reference
catalogs become create/edit/delete-able on the RBAC foundation.
API:
- Policy gains archivedAt (soft-delete); list/browser default to
archivedAt=null with ?includeArchived opt-in.
- PoliciesService: header create/update/archive/restore (customer FK
validated for a clean 404); add/update/remove for installments,
vehicles, drivers, beneficiaries, claims — each scoped to its policy so
one policy's id can't touch another's rows; lookups CRUD for providers,
policy types, adjusters.
- PoliciesController write routes: header create/update need STAFF+
(policy:create/update), archive/restore need MANAGER+ (policy:delete),
every child route needs policy:update. New LookupsController at /lookups
(read open; mutate needs lookup:manage / MANAGER+). Mutations audited.
- DTOs (policy header, children, lookups); dates coerced; shared coerce.ts.
Web:
- Generic ChildCollection editor (config-driven add/edit/remove table),
reused by both the policy detail child editors and the catalogs screen.
- PolicyForm (header) with type/provider selects and a debounced
CustomerPicker; /polizas/nuevo (accepts ?customerId prefill) and
/polizas/[id]/editar. Policy detail: gated action bar (Editar/Archivar)
+ "Administrar detalles" child editors for all five collections.
- /catalogos admin screen (aseguradoras/tipos/ajustadores), nav-gated on
lookup:manage. "Nueva póliza" buttons on the list and on the customer
detail (prefilled). api.ts + types for all of the above.
Verified against dev: policy create (dates coerced, archivedAt null),
installment/vehicle add, VIEWER child-add 403, cross-policy child guard
404, lookups CRUD with VIEWER 403 / MANAGER 201, archive drops from the
default list and includeArchived surfaces it. Both apps compile clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the RBAC foundation the CRUD phases build on, and the first write
module (users). The platform was read-only: every controller was guarded
only by AuthenticatedGuard and UserRole was ADMIN|STAFF. The old PHP app
stored level+role but enforced neither, so this is a fresh design.
Permission model (server-authoritative):
- UserRole expanded to an ordered rank ADMIN > MANAGER > STAFF > VIEWER.
VIEWER is the read-only role; STAFF+ can write.
- auth/abilities.ts: ROLE_RANK + ABILITY_MIN matrix + can()/abilitiesFor().
- @RequireAbility decorator + AbilityGuard enforce it on write routes;
reads stay on AuthenticatedGuard so any logged-in user can read.
- /auth/login and /auth/me now return the resolved abilities map, so the
web gates its UI off one payload instead of duplicating the rules.
User management (ADMIN-only, ability "user:manage"):
- UsersService gains list/create/update/resetPassword (argon2), never
returns passwordHash; blocks self-deactivation and self-demotion;
maps duplicate email to 409.
- UsersController: GET/POST /users, PATCH /users/:id,
POST /users/:id/reset-password.
- Every mutation logged via new AuditService over the existing
ActivityLog model (global CommonModule).
Web:
- AuthContext + useAuth/useCan; AppShell provides the user and gates the
new "Usuarios" nav entry on user:manage; shows the user's role.
- /usuarios admin page: list + create/edit form + password reset +
active toggle, Spanish-first, reusing existing card/table/field styles.
Schema pushed to dev (enum only, non-destructive). Verified end-to-end
against dev: admin CRUD works, VIEWER writes 403 while reads 200,
self-lockout guards and duplicate-email 409 all hold.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>