mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 11:55:42 +00:00
fix issue: user trade access
This commit is contained in:
@@ -3020,6 +3020,7 @@ export class BookingBatchService implements OnModuleInit {
|
|||||||
: booking.status;
|
: booking.status;
|
||||||
await manager.getRepository(Booking).update(bookingId, {
|
await manager.getRepository(Booking).update(bookingId, {
|
||||||
trainScheduleId: newScheduleId,
|
trainScheduleId: newScheduleId,
|
||||||
|
scheduledDate: schedule.scheduledDepartureDate,
|
||||||
status: restoredStatus,
|
status: restoredStatus,
|
||||||
// A paid booking still hunting for a wagon keeps its flag through the
|
// A paid booking still hunting for a wagon keeps its flag through the
|
||||||
// move — it only clears when wagons are actually assigned.
|
// move — it only clears when wagons are actually assigned.
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { applyDirectionScope, scopedDirections } from './trade-scope.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scope decides what a restricted user may see, so the cases that matter
|
||||||
|
* are the ones where a wrong answer widens access: an unrestricted fallback
|
||||||
|
* where a restriction was configured, or an out-of-scope explicit filter
|
||||||
|
* being honoured instead of denied.
|
||||||
|
*/
|
||||||
|
describe('scopedDirections', () => {
|
||||||
|
it('leaves an unrestricted user unfiltered', () => {
|
||||||
|
expect(scopedDirections(null)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('honours an explicit filter for an unrestricted user', () => {
|
||||||
|
expect(scopedDirections(null, 'EXPORT')).toEqual(['EXPORT']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the full scope when no filter is requested', () => {
|
||||||
|
expect(scopedDirections(['EXPORT'])).toEqual(['EXPORT']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('narrows to the intersection when the filter is in scope', () => {
|
||||||
|
expect(scopedDirections(['IMPORT', 'EXPORT'], 'EXPORT')).toEqual(['EXPORT']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies an out-of-scope filter instead of widening access', () => {
|
||||||
|
expect(scopedDirections(['EXPORT'], 'IMPORT')).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('applyDirectionScope', () => {
|
||||||
|
const makeQb = () => {
|
||||||
|
const calls: { sql: string; params?: object }[] = [];
|
||||||
|
const qb = {
|
||||||
|
calls,
|
||||||
|
andWhere(sql: string, params?: object) {
|
||||||
|
calls.push({ sql, params });
|
||||||
|
return qb;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return qb;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('does not touch the query when unrestricted', () => {
|
||||||
|
const qb = makeQb();
|
||||||
|
applyDirectionScope(qb as never, 'booking.trade_direction', null);
|
||||||
|
expect(qb.calls).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches nothing on an empty scope rather than everything', () => {
|
||||||
|
const qb = makeQb();
|
||||||
|
applyDirectionScope(qb as never, 'booking.trade_direction', []);
|
||||||
|
expect(qb.calls[0].sql).toBe('1 = 0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters to the allowed directions', () => {
|
||||||
|
const qb = makeQb();
|
||||||
|
applyDirectionScope(qb as never, 'booking.trade_direction', ['EXPORT']);
|
||||||
|
expect(qb.calls[0].sql).toContain('booking.trade_direction IN');
|
||||||
|
expect(qb.calls[0].params).toEqual({
|
||||||
|
scopeDirs_booking_trade_direction: ['EXPORT'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,10 +2,6 @@ import { useMemo, useState } from "react";
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
import {
|
|
||||||
useAllExternalUsers,
|
|
||||||
userTypeEnum,
|
|
||||||
} from "@/super-admin/hooks/useExternalUsers";
|
|
||||||
import {
|
import {
|
||||||
ALL_TRADE_DIRECTIONS,
|
ALL_TRADE_DIRECTIONS,
|
||||||
TRADE_DIRECTION_LABELS,
|
TRADE_DIRECTION_LABELS,
|
||||||
@@ -39,9 +35,9 @@ export default function TradeAccessPage() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const { data: usersResponse, isLoading: usersLoading } = useAllExternalUsers({
|
const { data: usersResponse, isLoading: usersLoading } = useQuery({
|
||||||
userType: userTypeEnum.employee,
|
queryKey: ["staff-users", "employees"],
|
||||||
take: 3000,
|
queryFn: userTradeAccessService.employees,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: configs, isLoading: configsLoading } = useQuery({
|
const { data: configs, isLoading: configsLoading } = useQuery({
|
||||||
|
|||||||
@@ -25,7 +25,27 @@ export interface MyTradeAccess {
|
|||||||
directions: TradeDirection[];
|
directions: TradeDirection[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StaffUser {
|
||||||
|
id: string;
|
||||||
|
/** Localized jsonb on iam.users — not a plain string. */
|
||||||
|
name: { en?: string; am?: string } | null;
|
||||||
|
username: string;
|
||||||
|
email: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export const userTradeAccessService = {
|
export const userTradeAccessService = {
|
||||||
|
/**
|
||||||
|
* Employees to assign scopes to. Served by the freight API rather than IAM's
|
||||||
|
* `/users/filter`, which 400s on its own pagination params (its @Query() DTO
|
||||||
|
* omits skip/take/orderBy while the global whitelist pipe rejects them).
|
||||||
|
*/
|
||||||
|
employees: async (): Promise<{ items: StaffUser[] }> =>
|
||||||
|
(
|
||||||
|
await client.get("/staff/users", {
|
||||||
|
params: { userType: "employee", pageSize: 100 },
|
||||||
|
})
|
||||||
|
).data,
|
||||||
|
|
||||||
/** All configured per-user scopes (admin only). */
|
/** All configured per-user scopes (admin only). */
|
||||||
list: async (): Promise<UserTradeAccessRow[]> =>
|
list: async (): Promise<UserTradeAccessRow[]> =>
|
||||||
(await client.get("/user-trade-access")).data,
|
(await client.get("/user-trade-access")).data,
|
||||||
|
|||||||
Reference in New Issue
Block a user