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>
47 lines
1.4 KiB
TypeScript
47 lines
1.4 KiB
TypeScript
import { Controller, Get, HttpCode, Post, Req, Res, UseGuards } from "@nestjs/common";
|
|
import { Request, Response } from "express";
|
|
import { LocalAuthGuard } from "./local-auth.guard";
|
|
import { AuthenticatedGuard } from "./authenticated.guard";
|
|
import { LoginDto } from "./login.dto";
|
|
import { abilitiesFor, Role } from "./abilities";
|
|
|
|
/** Attach the resolved ability map so the web can gate its UI off one payload. */
|
|
function withAbilities(user: unknown) {
|
|
const u = user as { role?: Role } | undefined;
|
|
if (!u?.role) return u;
|
|
return { ...u, abilities: abilitiesFor(u.role) };
|
|
}
|
|
|
|
@Controller("auth")
|
|
export class AuthController {
|
|
// LoginDto is only used for request-shape documentation/validation here —
|
|
// the actual credential check happens inside LocalStrategy via Passport,
|
|
// which populates req.user before this handler runs.
|
|
@UseGuards(LocalAuthGuard)
|
|
@Post("login")
|
|
@HttpCode(200)
|
|
login(@Req() req: Request, @Res({ passthrough: true }) _res: Response, _body?: LoginDto) {
|
|
return withAbilities(req.user);
|
|
}
|
|
|
|
@UseGuards(AuthenticatedGuard)
|
|
@Get("me")
|
|
me(@Req() req: Request) {
|
|
return withAbilities(req.user);
|
|
}
|
|
|
|
@Post("logout")
|
|
@HttpCode(200)
|
|
logout(@Req() req: Request) {
|
|
return new Promise((resolve, reject) => {
|
|
req.logout((err) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
resolve({ success: true });
|
|
});
|
|
});
|
|
}
|
|
}
|