feat(auth): role-based permissions + user management (plan phase 1)

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>
This commit is contained in:
2026-07-23 12:02:00 -07:00
co-authored by Claude Opus 4.8
parent d9f9e8a920
commit 74e2ad8bcd
21 changed files with 912 additions and 24 deletions
+34
View File
@@ -267,12 +267,46 @@ button {
font-size: 13px;
color: rgba(242, 239, 231, 0.7);
}
.appbar-user-name {
display: inline-flex;
flex-direction: column;
line-height: 1.2;
}
.appbar-user-role {
font-size: 11px;
color: rgba(242, 239, 231, 0.45);
}
@media (max-width: 640px) {
.appbar-user .appbar-user-name {
display: none;
}
}
/* ============================================================================
Admin form grid (users, and future CRUD forms)
========================================================================== */
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 18px;
flex-wrap: wrap;
}
.row-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.inline-form-note {
font-size: 13px;
color: var(--muted, #6b7280);
margin: 4px 0 14px;
}
/* ============================================================================
Buttons
========================================================================== */