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
70 lines
3.0 KiB
TypeScript
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 });
|
|
}
|