import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'; import { OrdersService } from './orders.service'; import { CreateOrderDto } from './dto/create-order.dto'; import { UpdateOrderStatusDto } from './dto/update-order-status.dto'; import { RequirePermission } from '../auth/guards'; import { CurrentCustomer, CustomerAuth } from '../customer-auth/customer.guards'; import type { AuthenticatedCustomer } from '../customer-auth/customer-jwt.strategy'; /** * Order routes. * * All three were `@Public()` until the security hotfix. The reference's * customer-facing order endpoints sat behind its `api-customer` guard and read * the customer from the token (`auth()->id()`, * henza_server ApiMobileController.php:533); the target had no customer session * at the time, so the id travelled in the body and the server trusted it. * That meant an anonymous caller could place an order on anyone's account — * and an order lands on that customer's gold debt and ledger. Now that the * customer token exists, the reference's arrangement is restored. */ @Controller('orders') export class OrdersController { constructor(private readonly orders: OrdersService) {} /** The customer comes from the token; the body's `customerId` is ignored. */ @CustomerAuth() @Post() create(@Body() dto: CreateOrderDto, @CurrentCustomer() me: AuthenticatedCustomer) { return this.orders.createOrder(dto, me.id); } /** * Unscoped read for operators. * * Declared BEFORE the `:id` route below: Nest matches in declaration order, * so a literal segment that could also be read as a parameter has to come * first or it is never reached. */ @Get('admin/:id') @RequirePermission('order.list') getAsAdmin(@Param('id') id: string) { return this.orders.getById(id); } /** * A customer reading one of their own orders. Scoped by token; someone * else's order is a 404 rather than a 403, so ids stay unenumerable. * * Mounted under /orders/:id (not /customer/me/orders/:id) so the existing * client paths keep working; it is the scoping that matters, not the shape. */ @CustomerAuth() @Get(':id') get(@Param('id') id: string, @CurrentCustomer() me: AuthenticatedCustomer) { return this.orders.getOwnedById(id, me.id); } @Patch(':id/status') @RequirePermission('order.list.sts.submit') updateStatus(@Param('id') id: string, @Body() dto: UpdateOrderStatusDto) { return this.orders.updateStatus(id, dto.status, dto.actorAdminUserId, dto.note); } }