UDT-009: Overrides de PermisosJson por usuario — cierre módulo Auth #12
7
src/web/src/features/users/api/getUserPermisos.ts
Normal file
7
src/web/src/features/users/api/getUserPermisos.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { axiosClient } from '@/api/axiosClient'
|
||||||
|
import type { UsuarioPermisos } from '../types'
|
||||||
|
|
||||||
|
export async function getUserPermisos(id: number): Promise<UsuarioPermisos> {
|
||||||
|
const response = await axiosClient.get<UsuarioPermisos>(`/api/v1/users/${id}/permisos`)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { axiosClient } from '@/api/axiosClient'
|
||||||
|
import type { UsuarioPermisos, UpdatePermisosOverridesPayload } from '../types'
|
||||||
|
|
||||||
|
export async function updateUserPermisosOverrides(
|
||||||
|
id: number,
|
||||||
|
payload: UpdatePermisosOverridesPayload,
|
||||||
|
): Promise<UsuarioPermisos> {
|
||||||
|
const response = await axiosClient.put<UsuarioPermisos>(
|
||||||
|
`/api/v1/users/${id}/permisos/overrides`,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { updateUserPermisosOverrides } from '../api/updateUserPermisosOverrides'
|
||||||
|
import type { UpdatePermisosOverridesPayload, UsuarioPermisos } from '../types'
|
||||||
|
import { userPermisosQueryKey } from './useUserPermisos'
|
||||||
|
import { userQueryKey } from './useUser'
|
||||||
|
|
||||||
|
export function useUpdateUserPermisosOverrides(userId: number) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation<UsuarioPermisos, Error, UpdatePermisosOverridesPayload>({
|
||||||
|
mutationFn: (payload) => updateUserPermisosOverrides(userId, payload),
|
||||||
|
onSuccess: () => {
|
||||||
|
// Invalidate the specific user's permisos query
|
||||||
|
queryClient.invalidateQueries({ queryKey: userPermisosQueryKey(userId) })
|
||||||
|
// Invalidate the user detail query (in case effective permisos affect UI elsewhere)
|
||||||
|
queryClient.invalidateQueries({ queryKey: userQueryKey(userId) })
|
||||||
|
// Invalidate the users list
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
15
src/web/src/features/users/hooks/useUserPermisos.ts
Normal file
15
src/web/src/features/users/hooks/useUserPermisos.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { getUserPermisos } from '../api/getUserPermisos'
|
||||||
|
import type { UsuarioPermisos } from '../types'
|
||||||
|
import type { UseQueryResult } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
export const userPermisosQueryKey = (id: number) => ['users', id, 'permisos'] as const
|
||||||
|
|
||||||
|
export function useUserPermisos(id: number): UseQueryResult<UsuarioPermisos> {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: userPermisosQueryKey(id),
|
||||||
|
queryFn: () => getUserPermisos(id),
|
||||||
|
staleTime: 15_000,
|
||||||
|
enabled: id > 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -46,3 +46,21 @@ export interface UpdateUserPayload {
|
|||||||
rol: string
|
rol: string
|
||||||
activo: boolean
|
activo: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UDT-009 — Permisos overrides per-user types
|
||||||
|
|
||||||
|
export interface UsuarioPermisos {
|
||||||
|
usuarioId: number
|
||||||
|
rol: string
|
||||||
|
rolPermisos: string[]
|
||||||
|
grant: string[]
|
||||||
|
deny: string[]
|
||||||
|
effective: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdatePermisosOverridesPayload {
|
||||||
|
grant: string[]
|
||||||
|
deny: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PermisoOverrideState = 'heredado' | 'concedido' | 'denegado'
|
||||||
|
|||||||
50
src/web/src/tests/features/users/getUserPermisos.test.ts
Normal file
50
src/web/src/tests/features/users/getUserPermisos.test.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'
|
||||||
|
import { http, HttpResponse } from 'msw'
|
||||||
|
import { setupServer } from 'msw/node'
|
||||||
|
import { getUserPermisos } from '../../../features/users/api/getUserPermisos'
|
||||||
|
|
||||||
|
const API_URL = 'http://localhost:5000'
|
||||||
|
|
||||||
|
const mockUsuarioPermisos = {
|
||||||
|
usuarioId: 42,
|
||||||
|
rol: 'cajero',
|
||||||
|
rolPermisos: ['ventas:contado:crear', 'ventas:contado:cobrar'],
|
||||||
|
grant: ['textos:editar'],
|
||||||
|
deny: ['ventas:contado:cobrar'],
|
||||||
|
effective: ['ventas:contado:crear', 'textos:editar'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = setupServer()
|
||||||
|
|
||||||
|
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||||
|
afterEach(() => server.resetHandlers())
|
||||||
|
afterAll(() => server.close())
|
||||||
|
|
||||||
|
describe('getUserPermisos api client', () => {
|
||||||
|
it('calls GET /api/v1/users/:id/permisos and returns UsuarioPermisos shape', async () => {
|
||||||
|
server.use(
|
||||||
|
http.get(`${API_URL}/api/v1/users/42/permisos`, () =>
|
||||||
|
HttpResponse.json(mockUsuarioPermisos),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await getUserPermisos(42)
|
||||||
|
|
||||||
|
expect(result.usuarioId).toBe(42)
|
||||||
|
expect(result.rol).toBe('cajero')
|
||||||
|
expect(result.rolPermisos).toEqual(['ventas:contado:crear', 'ventas:contado:cobrar'])
|
||||||
|
expect(result.grant).toEqual(['textos:editar'])
|
||||||
|
expect(result.deny).toEqual(['ventas:contado:cobrar'])
|
||||||
|
expect(result.effective).toEqual(['ventas:contado:crear', 'textos:editar'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects with error on 404', async () => {
|
||||||
|
server.use(
|
||||||
|
http.get(`${API_URL}/api/v1/users/9999/permisos`, () =>
|
||||||
|
HttpResponse.json({ title: 'Not Found', status: 404 }, { status: 404 }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(getUserPermisos(9999)).rejects.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'
|
||||||
|
import { http, HttpResponse } from 'msw'
|
||||||
|
import { setupServer } from 'msw/node'
|
||||||
|
import { updateUserPermisosOverrides } from '../../../features/users/api/updateUserPermisosOverrides'
|
||||||
|
|
||||||
|
const API_URL = 'http://localhost:5000'
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
usuarioId: 42,
|
||||||
|
rol: 'cajero',
|
||||||
|
rolPermisos: ['ventas:contado:crear'],
|
||||||
|
grant: ['textos:editar'],
|
||||||
|
deny: [],
|
||||||
|
effective: ['ventas:contado:crear', 'textos:editar'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = setupServer()
|
||||||
|
|
||||||
|
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||||
|
afterEach(() => server.resetHandlers())
|
||||||
|
afterAll(() => server.close())
|
||||||
|
|
||||||
|
describe('updateUserPermisosOverrides api client', () => {
|
||||||
|
it('calls PUT /api/v1/users/:id/permisos/overrides with correct body and returns UsuarioPermisos', async () => {
|
||||||
|
let capturedBody: unknown = null
|
||||||
|
server.use(
|
||||||
|
http.put(`${API_URL}/api/v1/users/42/permisos/overrides`, async ({ request }) => {
|
||||||
|
capturedBody = await request.json()
|
||||||
|
return HttpResponse.json(mockResponse)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await updateUserPermisosOverrides(42, {
|
||||||
|
grant: ['textos:editar'],
|
||||||
|
deny: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.grant).toEqual(['textos:editar'])
|
||||||
|
expect(result.effective).toContain('textos:editar')
|
||||||
|
expect(capturedBody).toMatchObject({ grant: ['textos:editar'], deny: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects on 400 invalid-permiso-codes', async () => {
|
||||||
|
server.use(
|
||||||
|
http.put(`${API_URL}/api/v1/users/42/permisos/overrides`, () =>
|
||||||
|
HttpResponse.json(
|
||||||
|
{ title: 'invalid-permiso-codes', status: 400, invalidCodes: ['modulo:fake:accion'] },
|
||||||
|
{ status: 400 },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
updateUserPermisosOverrides(42, { grant: ['modulo:fake:accion'], deny: [] }),
|
||||||
|
).rejects.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects on 400 grant-deny-overlap', async () => {
|
||||||
|
server.use(
|
||||||
|
http.put(`${API_URL}/api/v1/users/42/permisos/overrides`, () =>
|
||||||
|
HttpResponse.json(
|
||||||
|
{ title: 'grant-deny-overlap', status: 400, overlap: ['textos:editar'] },
|
||||||
|
{ status: 400 },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
updateUserPermisosOverrides(42, { grant: ['textos:editar'], deny: ['textos:editar'] }),
|
||||||
|
).rejects.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user