feat(web): api + hooks permisos overrides [UDT-009]
This commit is contained in:
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
|
||||
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