Files
edr-platform/apps/edr-freight-api/src/modules/support-chat/support-chat.controller.ts
Nathnael 0114673120 feat(auth): gate and regate freight API controllers
Gates the previously open support-agent, procurement, compliance,
facilities, list-users and trade-access controllers, separates customer
from staff routes across bookings, contracts, companies, billing,
warehouses, files and train scheduling, and moves billing, overview,
reports and the settings controllers onto their own keys instead of the
blanket admin key. Drops the demo-permissions module and the untested
notification test route.
2026-08-07 07:32:25 +00:00

117 lines
3.4 KiB
TypeScript

import { CurrentUser } from "@edr/api-common";
import {
SUPPORT_ATTACHMENT_MAX_PER_MESSAGE,
SupportAuthorRole,
} from "@edr/types";
import {
Body,
Controller,
Get,
Post,
Query,
UploadedFiles,
UseInterceptors,
} from "@nestjs/common";
import { FilesInterceptor } from "@nestjs/platform-express";
import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from "@nestjs/swagger";
import {
AuthUserPayload,
resolveAuthUserId,
} from "../../common/resolve-auth-user-id";
import {
SUPPORT_ATTACHMENT_FIELD,
supportAttachmentMulterOptions,
} from "./attachment-upload.options";
import { PortalCustomer } from "../../common/booking-guards";
import { ListMessagesQueryDto } from "./dto/list-messages-query.dto";
import { SendMessageDto } from "./dto/send-message.dto";
import { SupportChatService } from "./support-chat.service";
/**
* Portal (customer) support-chat endpoints. The caller's company has exactly one
* thread, so these are addressed as a singleton — no conversation id on the wire,
* and nothing for a portal user to pick between.
*/
@ApiTags("support-chat")
@Controller("support")
@PortalCustomer()
export class SupportChatController {
constructor(private readonly service: SupportChatService) {}
@Get("conversation")
@ApiOperation({
summary: "My company's support thread (null until someone speaks)",
})
conversation(@CurrentUser() user: AuthUserPayload) {
return this.service.getCustomerConversation(resolveAuthUserId(user));
}
@Get("conversation/messages")
@ApiOperation({
summary: "Messages in my company's support thread (newest page first)",
description:
"Keyset-paginated backwards from the newest message. Omit `before` for " +
"the newest page, then pass the previous response's `nextCursor` to walk " +
"back through history. `nextCursor: null` means the thread's start.",
})
messages(
@CurrentUser() user: AuthUserPayload,
@Query() query: ListMessagesQueryDto,
) {
return this.service.getCustomerMessages(resolveAuthUserId(user), query);
}
@Post("conversation/messages")
@UseInterceptors(
FilesInterceptor(
SUPPORT_ATTACHMENT_FIELD,
SUPPORT_ATTACHMENT_MAX_PER_MESSAGE,
supportAttachmentMulterOptions,
),
)
@ApiConsumes("multipart/form-data", "application/json")
@ApiBody({
schema: {
type: "object",
properties: {
body: { type: "string" },
attachments: {
type: "array",
items: { type: "string", format: "binary" },
},
},
},
})
@ApiOperation({
summary:
"Send a message as the customer (optionally with attachments), opening the thread if needed",
})
send(
@CurrentUser() user: AuthUserPayload,
@Body() body: SendMessageDto,
@UploadedFiles() attachments?: Express.Multer.File[],
) {
return this.service.sendAsCustomer(
resolveAuthUserId(user),
body.body,
attachments ?? [],
);
}
@Post("conversation/read")
@ApiOperation({ summary: "Mark my company's thread read (customer side)" })
read(@CurrentUser() user: AuthUserPayload) {
return this.service.markCustomerRead(resolveAuthUserId(user));
}
@Get("unread-count")
@ApiOperation({ summary: "Count my unread support messages" })
unread(@CurrentUser() user: AuthUserPayload) {
return this.service.unreadCount(
SupportAuthorRole.CUSTOMER,
resolveAuthUserId(user),
);
}
}