From 0ff7bc46fc3ac0ab636996f56f356909d50d1b65 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 6 May 2026 21:04:50 +0100 Subject: [PATCH] Move chat to private authenticated API --- apps/api/src/chat/routes.ts | 44 ++++++ apps/api/src/chat/store.ts | 62 ++++++++ apps/api/src/server.ts | 2 + apps/web/src/components/ChatDrawer.vue | 2 +- apps/web/src/services/api.ts | 40 ++++- apps/web/src/services/chat.ts | 208 +++++-------------------- apps/web/src/types.ts | 18 +++ 7 files changed, 202 insertions(+), 174 deletions(-) create mode 100644 apps/api/src/chat/routes.ts create mode 100644 apps/api/src/chat/store.ts diff --git a/apps/api/src/chat/routes.ts b/apps/api/src/chat/routes.ts new file mode 100644 index 0000000..bc05abc --- /dev/null +++ b/apps/api/src/chat/routes.ts @@ -0,0 +1,44 @@ +import { Router } from "express"; +import { z } from "zod"; +import { requireAuth } from "../auth/middleware.js"; +import { addMessage, addReaction, listChat } from "./store.js"; + +export const chatRouter = Router(); + +const MessageBody = z.object({ + content: z.string().trim().min(1).max(2000), + replyToId: z.string().max(128).optional(), + replyToPubkey: z.string().max(128).optional(), +}); + +const ReactionBody = z.object({ + messageId: z.string().min(1).max(128), + content: z.string().trim().min(1).max(32), +}); + +chatRouter.use(requireAuth); + +chatRouter.get("/", (_req, res) => { + res.json(listChat()); +}); + +chatRouter.post("/messages", (req, res) => { + const body = MessageBody.parse(req.body); + const message = addMessage({ + pubkey: req.session!.pubkey, + content: body.content, + ...(body.replyToId ? { replyToId: body.replyToId } : {}), + ...(body.replyToPubkey ? { replyToPubkey: body.replyToPubkey } : {}), + }); + res.status(201).json(message); +}); + +chatRouter.post("/reactions", (req, res) => { + const body = ReactionBody.parse(req.body); + const reaction = addReaction({ + pubkey: req.session!.pubkey, + messageId: body.messageId, + content: body.content, + }); + res.status(201).json(reaction); +}); diff --git a/apps/api/src/chat/store.ts b/apps/api/src/chat/store.ts new file mode 100644 index 0000000..ed80a02 --- /dev/null +++ b/apps/api/src/chat/store.ts @@ -0,0 +1,62 @@ +export type StoredChatMessage = { + id: string; + pubkey: string; + content: string; + createdAt: number; + replyToId: string; + replyToPubkey: string; +}; + +export type StoredChatReaction = { + id: string; + pubkey: string; + messageId: string; + content: string; + createdAt: number; +}; + +const MAX_MESSAGES = 500; +const MAX_REACTIONS = 2000; + +const messages: StoredChatMessage[] = []; +const reactions: StoredChatReaction[] = []; + +export function listChat(): { messages: StoredChatMessage[]; reactions: StoredChatReaction[] } { + return { messages: [...messages], reactions: [...reactions] }; +} + +export function addMessage(input: { + pubkey: string; + content: string; + replyToId?: string; + replyToPubkey?: string; +}): StoredChatMessage { + const message: StoredChatMessage = { + id: crypto.randomUUID(), + pubkey: input.pubkey, + content: input.content, + createdAt: Math.floor(Date.now() / 1000), + replyToId: input.replyToId ?? "", + replyToPubkey: input.replyToPubkey ?? "", + }; + messages.push(message); + if (messages.length > MAX_MESSAGES) messages.splice(0, messages.length - MAX_MESSAGES); + return message; +} + +export function addReaction(input: { + pubkey: string; + messageId: string; + content: string; +}): StoredChatReaction { + const reaction: StoredChatReaction = { + id: crypto.randomUUID(), + pubkey: input.pubkey, + messageId: input.messageId, + content: input.content, + createdAt: Math.floor(Date.now() / 1000), + }; + reactions.push(reaction); + if (reactions.length > MAX_REACTIONS) reactions.splice(0, reactions.length - MAX_REACTIONS); + return reaction; +} diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index b0c63cb..6b744e8 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -9,6 +9,7 @@ import fs from "node:fs"; import { config } from "./config.js"; import { logger } from "./logger.js"; import { authRouter } from "./auth/routes.js"; +import { chatRouter } from "./chat/routes.js"; import { datumRouter } from "./datum/routes.js"; import { errorHandler } from "./errors.js"; @@ -61,6 +62,7 @@ export function buildApp() { res.json({ ok: true }); }); app.use("/api/auth", authRouter); + app.use("/api/chat", chatRouter); app.use("/api/datum", datumRouter); const staticDir = config.staticDir diff --git a/apps/web/src/components/ChatDrawer.vue b/apps/web/src/components/ChatDrawer.vue index f96c198..d308761 100644 --- a/apps/web/src/components/ChatDrawer.vue +++ b/apps/web/src/components/ChatDrawer.vue @@ -204,7 +204,7 @@ async function scrollBottom(): Promise {