Files
thbd-portal/src/app/api/chat/conversations/[id]/messages/route.ts
T
ogrechkoandClaude Sonnet 5 579528edf7 Initial commit: thbd-portal, duplicated from elmi-portal
Rebranded for Thunderbird Portal (THBD): numbering prefix EL->TH, all
"ELMI"/"ЭЛМИ" UI strings genericized, ELMI's real contract templates and
company legal/bank details removed (not applicable to this company), fresh
VAPID keypair and encryption key, fresh single-migration schema history.
Same architecture as elmi-portal: Web Push notifications, unread-chat app
badge, cookie-session auth.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gh2UXUQUVBWroWEnn1FLFG
2026-08-31 16:33:30 +00:00

70 lines
3.0 KiB
TypeScript

export const runtime = "nodejs";
import { NextResponse } from "next/server";
import { requireSession } from "@/lib/auth/require";
import { listMessages, sendMessage } from "@/lib/chat/service";
import { saveChatAttachment } from "@/lib/chat/attachments";
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
const { session, response } = await requireSession();
if (!session) return response;
const { id } = await params;
const messages = await listMessages(id, session.user.id);
if (messages === null) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
return NextResponse.json(messages);
}
const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
/** Один эндпоинт на текст/вложение/голосовое — форма всегда multipart, поля опциональны в зависимости от того, что отправляет клиент. */
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { session, response } = await requireSession();
if (!session) return response;
const { id } = await params;
const form = await request.formData().catch(() => null);
if (!form) return NextResponse.json({ error: "Некорректные данные" }, { status: 400 });
const body = form.get("body");
const replyToMessageId = form.get("replyToMessageId");
const file = form.get("file");
const kindRaw = form.get("kind");
const bodyText = typeof body === "string" && body.trim() ? body.trim() : undefined;
if (!bodyText && !(file instanceof File)) {
return NextResponse.json({ error: "Пустое сообщение" }, { status: 400 });
}
if (file instanceof File && file.size > MAX_ATTACHMENT_BYTES) {
return NextResponse.json({ error: "Файл слишком большой (максимум 25 МБ)" }, { status: 400 });
}
let message;
try {
message = await sendMessage(id, session.user.id, {
body: bodyText,
replyToMessageId: typeof replyToMessageId === "string" && replyToMessageId ? replyToMessageId : undefined,
});
} catch (err) {
const msg = err instanceof Error ? err.message : "Не удалось отправить сообщение";
return NextResponse.json({ error: msg }, { status: 403 });
}
let attachment = null;
if (file instanceof File) {
const kind = kindRaw === "voice" ? "voice" : file.type.startsWith("image/") ? "image" : "file";
const durationRaw = form.get("durationSeconds");
const content = Buffer.from(await file.arrayBuffer());
attachment = await saveChatAttachment({
messageId: message.id,
kind,
fileName: file.name || (kind === "voice" ? "voice-message.webm" : "file"),
mimeType: file.type || "application/octet-stream",
content,
durationSeconds: typeof durationRaw === "string" ? Number(durationRaw) : undefined,
});
}
return NextResponse.json({ message, attachment }, { status: 201 });
}