mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge pull request #788 from Tria-plc/freight/feat/chat-app
Freight/feat/chat app added attachment
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
export * from "./payments";
|
||||
export * from "./payment-messaging";
|
||||
export * from "./support-attachments";
|
||||
|
||||
export interface BaseEntity {
|
||||
id: string;
|
||||
|
||||
73
packages/types/src/common/support-attachments.ts
Normal file
73
packages/types/src/common/support-attachments.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Rules shared by the freight and passenger support-chat attachment flows.
|
||||
*
|
||||
* The two chat backends are independent implementations (freight: TypeORM +
|
||||
* polymorphic `FileRecord`; passenger: Prisma + `SupportAttachment`), but the
|
||||
* *contract* a client codes against — what may be uploaded, how large, how the
|
||||
* preview URL behaves — must not drift between them. Keep the limits here so
|
||||
* both APIs validate identically and all four web apps can render one consistent
|
||||
* "file too large / type not allowed" message.
|
||||
*/
|
||||
|
||||
/** `FileRecord.resource` discriminator for freight chat attachments. */
|
||||
export const SUPPORT_ATTACHMENT_RESOURCE = "support_message";
|
||||
|
||||
/** Per-file ceiling. Enforced server-side; the UI pre-checks to fail fast. */
|
||||
export const SUPPORT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
/** Max files on a single message. */
|
||||
export const SUPPORT_ATTACHMENT_MAX_PER_MESSAGE = 10;
|
||||
|
||||
/**
|
||||
* How long a minted preview URL stays valid. Long enough that an open thread
|
||||
* doesn't rot mid-read, short enough that a leaked URL isn't a durable grant.
|
||||
*/
|
||||
export const SUPPORT_ATTACHMENT_URL_TTL_SECONDS = 60 * 60;
|
||||
|
||||
/**
|
||||
* Types accepted on a chat message.
|
||||
*
|
||||
* Deliberately NARROWER than `FilesService.ALLOWED_UPLOAD_MIME` (which also
|
||||
* serves generated PDFs and scanned business documents at 25MB). Chat is
|
||||
* user-to-user, so the blast radius of a bad file is another human clicking it.
|
||||
*
|
||||
* `image/svg+xml` is excluded on purpose and must stay excluded: an SVG is
|
||||
* executable markup, and previewing one inline (`<img>` is safe, but an
|
||||
* `<iframe>`/direct navigation is not) executes any script it carries under the
|
||||
* serving origin. Nothing in chat needs vector uploads.
|
||||
*/
|
||||
export const SUPPORT_ATTACHMENT_ALLOWED_MIME: readonly string[] = [
|
||||
// images (previewable inline)
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/gif",
|
||||
// documents
|
||||
"application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"text/csv",
|
||||
"text/plain",
|
||||
];
|
||||
|
||||
/** Image subset — these are the ones worth rendering as a thumbnail. */
|
||||
export const SUPPORT_ATTACHMENT_IMAGE_MIME: readonly string[] = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/gif",
|
||||
];
|
||||
|
||||
export function isSupportAttachmentImage(mimeType: string): boolean {
|
||||
return SUPPORT_ATTACHMENT_IMAGE_MIME.includes(mimeType);
|
||||
}
|
||||
|
||||
export function isSupportAttachmentAllowed(mimeType: string): boolean {
|
||||
return SUPPORT_ATTACHMENT_ALLOWED_MIME.includes(mimeType);
|
||||
}
|
||||
|
||||
/** `accept` attribute for a file input / paste target. */
|
||||
export const SUPPORT_ATTACHMENT_ACCEPT =
|
||||
SUPPORT_ATTACHMENT_ALLOWED_MIME.join(",");
|
||||
@@ -16,12 +16,37 @@
|
||||
* constants shared by the gateway (emitter) and both web apps (subscribers).
|
||||
*/
|
||||
|
||||
import { SUPPORT_ATTACHMENT_RESOURCE } from "../common/support-attachments";
|
||||
|
||||
/** Who authored a message — the customer side or a backoffice agent. */
|
||||
export enum SupportAuthorRole {
|
||||
CUSTOMER = "CUSTOMER",
|
||||
AGENT = "AGENT",
|
||||
}
|
||||
|
||||
/** `FileRecord.resource` value chat attachments are stored under. */
|
||||
export { SUPPORT_ATTACHMENT_RESOURCE };
|
||||
|
||||
/** A file attached to a support message. */
|
||||
export interface SupportAttachmentDto {
|
||||
/**
|
||||
* FileRecord id. For an authenticated download use
|
||||
* `GET /support/attachments/:id?download=1` — the generic `GET /files/:id`
|
||||
* route deliberately refuses chat attachments (it has no ownership check).
|
||||
*/
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
/** Bytes. */
|
||||
size: number;
|
||||
/**
|
||||
* Short-lived signed URL for inline preview (`<img src>`), minted per response.
|
||||
* Expires — see SUPPORT_ATTACHMENT_URL_TTL_SECONDS. Clients must not persist it;
|
||||
* refetch the thread to renew.
|
||||
*/
|
||||
url: string;
|
||||
}
|
||||
|
||||
/** A single chat message on the wire. */
|
||||
export interface SupportMessageDto {
|
||||
id: string;
|
||||
@@ -30,7 +55,9 @@ export interface SupportMessageDto {
|
||||
authorRole: SupportAuthorRole;
|
||||
/** Display name of the author, resolved at send time (best-effort). */
|
||||
authorName?: string | null;
|
||||
/** Empty string for attachment-only messages. */
|
||||
body: string;
|
||||
attachments: SupportAttachmentDto[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -54,9 +81,17 @@ export interface SupportConversationDto {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** Post a message. The portal omits the id; the thread is implied by the company. */
|
||||
/**
|
||||
* Post a message. The portal omits the id; the thread is implied by the company.
|
||||
*
|
||||
* Attachments do not travel in this shape — a message carrying files is sent as
|
||||
* `multipart/form-data` with a `body` field plus one or more `attachments` file
|
||||
* parts, so the files are written with `resourceId = message.id` in the same
|
||||
* request. There is no staging area and therefore no orphan-file GC to run.
|
||||
*/
|
||||
export interface SendSupportMessageDto {
|
||||
body: string;
|
||||
/** Optional only when the request carries at least one attachment. */
|
||||
body?: string;
|
||||
}
|
||||
|
||||
/** Agent opens a thread with a company that has none yet. */
|
||||
@@ -81,6 +116,24 @@ export interface SupportConversationListResult {
|
||||
unreadCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A page of messages, walking **backwards** from newest.
|
||||
*
|
||||
* Chat pages by keyset, not by offset: an inbound message while an agent is
|
||||
* scrolled back would shift every offset by one and duplicate/skip rows across
|
||||
* pages. The cursor pins a fixed point in (createdAt, id), so concurrent inserts
|
||||
* at the head can't disturb pages already read.
|
||||
*/
|
||||
export interface SupportMessageListResult {
|
||||
/** Oldest-first *within the page*, so a page appends/prepends as a block. */
|
||||
items: SupportMessageDto[];
|
||||
/**
|
||||
* Opaque cursor for the next (older) page; null when the thread's start has
|
||||
* been reached. Pass back as `before`.
|
||||
*/
|
||||
nextCursor: string | null;
|
||||
}
|
||||
|
||||
/** Socket.io event names pushed server → client on the `support-chat` namespace. */
|
||||
export const SUPPORT_CHAT_WS_EVENTS = {
|
||||
/** A new message was added to a conversation the socket can see. */
|
||||
|
||||
@@ -23,6 +23,33 @@ export enum PassengerSupportSender {
|
||||
AGENT = "AGENT",
|
||||
}
|
||||
|
||||
/**
|
||||
* A file attached to a passenger support message.
|
||||
*
|
||||
* Structurally identical to the freight `SupportAttachmentDto` — kept as its own
|
||||
* declaration because the two namespaces are independently versioned and the
|
||||
* backing stores differ (Prisma `SupportAttachment` here, polymorphic
|
||||
* `FileRecord` in freight). The upload rules themselves are shared: see
|
||||
* `SUPPORT_ATTACHMENT_*` in `common/support-attachments`.
|
||||
*/
|
||||
export interface PassengerSupportAttachmentDto {
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
/** Bytes. */
|
||||
size: number;
|
||||
/**
|
||||
* Short-lived signed URL — serves both inline preview and download. Expires
|
||||
* (see SUPPORT_ATTACHMENT_URL_TTL_SECONDS); clients must not persist it,
|
||||
* refetch the thread to renew.
|
||||
*
|
||||
* There is no API-streamed alternative here, unlike freight: this app has no
|
||||
* general file endpoint, so the signed URL is the only handle. It is minted
|
||||
* only into responses the caller was already authorized to receive.
|
||||
*/
|
||||
url: string;
|
||||
}
|
||||
|
||||
/** A single chat message on the wire. */
|
||||
export interface PassengerSupportMessageDto {
|
||||
id: string;
|
||||
@@ -30,7 +57,9 @@ export interface PassengerSupportMessageDto {
|
||||
sender: PassengerSupportSender;
|
||||
/** Display name of the author, best-effort. */
|
||||
authorName?: string | null;
|
||||
/** Empty string for attachment-only messages. */
|
||||
text: string;
|
||||
attachments: PassengerSupportAttachmentDto[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -74,15 +103,43 @@ export interface CreateGuestSupportConversationDto {
|
||||
initialMessage: string;
|
||||
}
|
||||
|
||||
/** Post a message into an existing conversation. */
|
||||
/**
|
||||
* Post a message into an existing conversation.
|
||||
*
|
||||
* As in freight, attachments travel as `multipart/form-data` (a `text` field
|
||||
* plus `attachments` file parts) rather than as ids in this body, so files are
|
||||
* persisted against the message that owns them in one request.
|
||||
*/
|
||||
export interface SendPassengerSupportMessageDto {
|
||||
text: string;
|
||||
/** Optional only when the request carries at least one attachment. */
|
||||
text?: string;
|
||||
}
|
||||
|
||||
/** The single device-scoped thread for the portal: conversation + its messages. */
|
||||
/**
|
||||
* The single device-scoped thread for the portal: conversation + its newest page
|
||||
* of messages.
|
||||
*
|
||||
* `messages` is the *first page only* (newest N, oldest-first within the page) —
|
||||
* it is not the whole thread. Page backwards with `nextCursor` via the messages
|
||||
* endpoint, exactly as the backoffice does.
|
||||
*/
|
||||
export interface PassengerSupportThreadDto {
|
||||
conversation: PassengerSupportConversationDto | null;
|
||||
messages: PassengerSupportMessageDto[];
|
||||
/** Cursor for the next (older) page; null when the thread's start is loaded. */
|
||||
nextCursor: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A page of messages, walking backwards from newest. Keyset — not offset — so a
|
||||
* message arriving while the reader is scrolled back cannot shift or duplicate
|
||||
* pages already fetched. See the freight twin for the full rationale.
|
||||
*/
|
||||
export interface PassengerSupportMessageListResult {
|
||||
/** Oldest-first within the page. */
|
||||
items: PassengerSupportMessageDto[];
|
||||
/** Pass back as `before`; null at the start of the thread. */
|
||||
nextCursor: string | null;
|
||||
}
|
||||
|
||||
/** Paginated list envelope for the conversations list endpoints. */
|
||||
|
||||
Reference in New Issue
Block a user