vibe coded v1
This commit is contained in:
132
api/routes/ws.ts
Normal file
132
api/routes/ws.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { verifyJWT } from "../lib/jwt.ts";
|
||||
import {
|
||||
broadcastPresence,
|
||||
broadcastVoteUpdate,
|
||||
getOnlineUsers,
|
||||
register,
|
||||
type WsClient,
|
||||
unregister,
|
||||
} from "../services/ws-service.ts";
|
||||
import { castVote, getUserVotes, removeVote } from "../services/vote-service.ts";
|
||||
import { getUserById } from "../services/user-service.ts";
|
||||
import { APIException } from "../model/interfaces.ts";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
function isAllowedOrigin(origin: string): boolean {
|
||||
if (!origin) return false;
|
||||
return /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin);
|
||||
}
|
||||
|
||||
router.get("/ws", async (ctx) => {
|
||||
const origin = ctx.request.headers.get("origin") ?? "";
|
||||
if (!isAllowedOrigin(origin)) {
|
||||
ctx.response.status = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.isUpgradable) {
|
||||
ctx.response.status = 426;
|
||||
return;
|
||||
}
|
||||
|
||||
const token = ctx.request.url.searchParams.get("token");
|
||||
const authPayload = token ? await verifyJWT(token) : null;
|
||||
|
||||
const socket = ctx.upgrade();
|
||||
|
||||
let avatarMime: string | undefined;
|
||||
if (authPayload) {
|
||||
try { avatarMime = getUserById(authPayload.userId).avatarMime; } catch { /* user not found */ }
|
||||
}
|
||||
|
||||
const client: WsClient = {
|
||||
socket,
|
||||
userId: authPayload?.userId,
|
||||
username: authPayload?.username,
|
||||
avatarMime,
|
||||
};
|
||||
|
||||
// Use addEventListener — more reliable than onopen= with Deno.serve
|
||||
socket.addEventListener("open", () => {
|
||||
register(client);
|
||||
broadcastPresence();
|
||||
|
||||
try {
|
||||
const myVotes = authPayload ? getUserVotes(authPayload.userId) : [];
|
||||
socket.send(JSON.stringify({
|
||||
type: "welcome",
|
||||
users: getOnlineUsers(),
|
||||
myVotes,
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error("[ws] welcome send failed:", err);
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener("message", (event) => {
|
||||
let msg: { type: string; dumpId?: string };
|
||||
try {
|
||||
msg = JSON.parse(event.data as string);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (msg.type) {
|
||||
case "ping":
|
||||
socket.send(JSON.stringify({ type: "pong" }));
|
||||
break;
|
||||
case "vote_cast":
|
||||
handleVote(client, msg.dumpId, "cast");
|
||||
break;
|
||||
case "vote_remove":
|
||||
handleVote(client, msg.dumpId, "remove");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener("close", () => {
|
||||
unregister(client);
|
||||
broadcastPresence();
|
||||
});
|
||||
});
|
||||
|
||||
function handleVote(
|
||||
client: WsClient,
|
||||
dumpId: string | undefined,
|
||||
action: "cast" | "remove",
|
||||
): void {
|
||||
const { socket } = client;
|
||||
|
||||
if (!client.userId) {
|
||||
socket.send(JSON.stringify({ type: "error", message: "Authentication required" }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dumpId) {
|
||||
socket.send(JSON.stringify({ type: "error", message: "Missing dumpId" }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newCount = action === "cast"
|
||||
? castVote(dumpId, client.userId)
|
||||
: removeVote(dumpId, client.userId);
|
||||
|
||||
socket.send(JSON.stringify({
|
||||
type: "vote_ack",
|
||||
dumpId,
|
||||
action,
|
||||
success: true,
|
||||
voteCount: newCount,
|
||||
}));
|
||||
|
||||
broadcastVoteUpdate(dumpId, newCount);
|
||||
} catch (err) {
|
||||
const message = err instanceof APIException ? err.message : "Vote failed";
|
||||
socket.send(JSON.stringify({ type: "error", message }));
|
||||
}
|
||||
}
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user