mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +00:00
FreightPermissionGuard now rejects non-employee user types before the key check, making every BookingStaff route staff-only in one place. Adds PortalCustomer and MixedAudience for the routes both audiences share, and stops ServiceAuthGuard failing open when SERVICE_AUTH_TOKEN is unset.
108 lines
3.4 KiB
TypeScript
108 lines
3.4 KiB
TypeScript
import {
|
|
CanActivate,
|
|
ExecutionContext,
|
|
ForbiddenException,
|
|
Injectable,
|
|
Type,
|
|
UnauthorizedException,
|
|
} from '@nestjs/common';
|
|
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
|
|
|
import { hasFreightPermission, isSuperAdmin } from './freight-permission.util';
|
|
|
|
// String literals on purpose (same reasoning as login-audience.middleware.ts):
|
|
// the values are wire-format constants from iam.users.user_type, and importing
|
|
// the vendored enum couples us to its package layout for no gain.
|
|
const CUSTOMER_USER_TYPES = ['individual', 'external_organization'];
|
|
|
|
const userTypeOf = (user: TCurrentUser): string | undefined =>
|
|
(user as { userType?: string }).userType;
|
|
|
|
/** Staff routes are employee-only; a missing userType (stale session) also fails. */
|
|
const isEmployee = (user: TCurrentUser): boolean =>
|
|
userTypeOf(user) === 'employee' || isSuperAdmin(user);
|
|
|
|
export function FreightPermissionGuard(
|
|
permissions: string[],
|
|
): Type<CanActivate> {
|
|
@Injectable()
|
|
class FreightPermissionsGuard implements CanActivate {
|
|
canActivate(context: ExecutionContext): boolean {
|
|
const request = context.switchToHttp().getRequest<{ user?: TCurrentUser }>();
|
|
const user = request.user;
|
|
|
|
if (!user) {
|
|
throw new UnauthorizedException('Authentication required');
|
|
}
|
|
if (!isEmployee(user)) {
|
|
throw new ForbiddenException('Staff account required');
|
|
}
|
|
|
|
if (!permissions?.length) return true;
|
|
if (permissions.some((p) => hasFreightPermission(user, p))) {
|
|
return true;
|
|
}
|
|
|
|
throw new ForbiddenException(
|
|
`Missing permission. Required one of: ${permissions.join(', ')}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return FreightPermissionsGuard;
|
|
}
|
|
|
|
/** Portal routes: customer accounts only (individual / external organization). */
|
|
@Injectable()
|
|
export class PortalCustomerGuard implements CanActivate {
|
|
canActivate(context: ExecutionContext): boolean {
|
|
const request = context.switchToHttp().getRequest<{ user?: TCurrentUser }>();
|
|
const user = request.user;
|
|
|
|
if (!user) {
|
|
throw new UnauthorizedException('Authentication required');
|
|
}
|
|
if (!CUSTOMER_USER_TYPES.includes(userTypeOf(user) ?? '')) {
|
|
throw new ForbiddenException('Customer account required');
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Routes both audiences legitimately call (contract sign, shared document
|
|
* reads, warehouse handover). Staff callers must hold one of the given
|
|
* permissions; customer callers pass here and are scoped by the service's
|
|
* ownership checks.
|
|
*/
|
|
export function MixedAudienceGuard(permissions: string[]): Type<CanActivate> {
|
|
@Injectable()
|
|
class MixedAudiencesGuard implements CanActivate {
|
|
canActivate(context: ExecutionContext): boolean {
|
|
const request = context.switchToHttp().getRequest<{ user?: TCurrentUser }>();
|
|
const user = request.user;
|
|
|
|
if (!user) {
|
|
throw new UnauthorizedException('Authentication required');
|
|
}
|
|
if (CUSTOMER_USER_TYPES.includes(userTypeOf(user) ?? '')) {
|
|
return true;
|
|
}
|
|
if (!isEmployee(user)) {
|
|
throw new ForbiddenException('Unrecognized account type');
|
|
}
|
|
if (
|
|
!permissions?.length ||
|
|
permissions.some((p) => hasFreightPermission(user, p))
|
|
) {
|
|
return true;
|
|
}
|
|
throw new ForbiddenException(
|
|
`Missing permission. Required one of: ${permissions.join(', ')}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return MixedAudiencesGuard;
|
|
}
|