import config from "@/config"; import _logger from "@/utils/logging"; import { db } from "@/database"; import { tickets, ticketMessages, moderators, sessions, type Ticket } from "@/database/schema"; import { desc, eq, asc } from "drizzle-orm"; import type { BunRequest } from "bun"; import { getUser } from "./discord"; const logger = _logger.child({ name: "server" }); const CLIENT_ID = config.discord.app_id const CLIENT_SECRET = config.discord.client_secret const REDIRECT_URI = 'http://127.0.0.1:3000/auth/callback'; const FRONTEND_URL = process.env.FRONTEND_URL || 'http://127.0.0.1:4321'; type AnonymisedTicket = Omit function anonymiseTicket(ticket: Ticket): AnonymisedTicket { const _ticket = ticket as Partial delete _ticket.authorId; return _ticket as AnonymisedTicket } async function getSessionUser(req: BunRequest) { const cookieHeader = req.headers.get("Cookie"); if (!cookieHeader) return null; const sessionCookie = cookieHeader.split("; ").find(c => c.startsWith("session=")); if (!sessionCookie) return null; const sessionId = sessionCookie.split("=")[1]; const sessionList = await db.select().from(sessions).where(eq(sessions.id, sessionId)).limit(1); if (sessionList.length === 0) return null; return { id: sessionList[0].userId, username: sessionList[0].username }; } const server = Bun.serve({ hostname: "127.0.0.1", routes: { "/login": () => { const redirectUri = "https://discord.com/oauth2/authorize?client_id=1350116449611677770&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fauth%2Fcallback&scope=identify+guilds" return new Response(null, { status: 302, headers: { Location: redirectUri, }, }); }, "/auth/callback": { GET: async (req) => { const url = new URL(req.url); const code = url.searchParams.get("code"); if (!code) return new Response("No code provided", { status: 400, statusText: "No code provided" }) const params = new URLSearchParams(); params.append("client_id", CLIENT_ID); params.append("client_secret", CLIENT_SECRET); params.append("grant_type", "authorization_code"); params.append("code", code); params.append("redirect_uri", REDIRECT_URI); const tokenResponse = await fetch("https://discord.com/api/oauth2/token", { method: "POST", body: params, headers: { "Content-Type": "application/x-www-form-urlencoded", }, }); const data = await tokenResponse.json(); console.log(data) if (data.error) return new Response(data.error_description, { status: 400, statusText: data.error }) const accessToken = data.access_token; const userResponse = await fetch("https://discord.com/api/users/@me", { headers: { Authorization: `Bearer ${accessToken}`, }, }); const userData = await userResponse.json(); const sessionId = crypto.randomUUID(); await db.insert(sessions).values({ id: sessionId, userId: userData.id, username: userData.username }); return new Response(null, { status: 302, headers: { Location: `${FRONTEND_URL}/tickets`, "Set-Cookie": `session=${sessionId}; Path=/; HttpOnly; SameSite=Lax`, }, }); } }, "/me": { GET: async (req: BunRequest) => { const user = await getSessionUser(req); if (!user) return new Response("Unauthorized", { status: 401 }); const modCheck = await db.select().from(moderators).where(eq(moderators.user_id, user.id)).limit(1); const isMod = modCheck.length > 0; return new Response(JSON.stringify({ ...user, isMod }), { headers: { "Content-Type": "application/json" } }); } }, "/tickets": { GET: async (req: BunRequest) => { const user = await getSessionUser(req); if (!user) return new Response(null, { status: 302, headers: { Location: "/login" } }); const modCheck = await db.select().from(moderators).where(eq(moderators.user_id, user.id)).limit(1); if (modCheck.length === 0) { return new Response("Forbidden: You must be a moderator to view tickets.", { status: 403 }); } const url = new URL(req.url); const limitStr = url.searchParams.get("limit") || "50"; const limit = parseInt(limitStr, 10); const allTickets = await db.select().from(tickets).limit(limit).orderBy(desc(tickets.createdAt)); const anonymisedTickets = allTickets.map(anonymiseTicket); return new Response(JSON.stringify(anonymisedTickets), { headers: { "Content-Type": "application/json" } }); } }, "/tickets/:id": { GET: async (req: BunRequest) => { const user = await getSessionUser(req); if (!user) return new Response(null, { status: 302, headers: { Location: "/login" } }); const modCheck = await db.select().from(moderators).where(eq(moderators.user_id, user.id)).limit(1); if (modCheck.length === 0) return new Response("Forbidden: You must be a moderator to view tickets.", { status: 403 }); const { id } = req.params; const ticketId = parseInt(id, 10); if (isNaN(ticketId)) return new Response("Invalid ID", { status: 400 }); const ticketList = await db.select().from(tickets).where(eq(tickets.id, ticketId)).limit(1); const _ticket = ticketList[0]; if (!_ticket) return new Response("Ticket not found", { status: 404 }); const ticket = anonymiseTicket(_ticket); const messages = await db.select().from(ticketMessages).where(eq(ticketMessages.ticketId, ticketId)).orderBy(asc(ticketMessages.createdAt)); const authors = [...new Set(messages.map(m => m.authorId))]; const authorData = await Promise.all(authors.map(async (authorId) => { try { return await getUser(authorId); } catch { return { id: authorId, username: "Unknown" }; } })); const newMessages = messages.map((message) => { const author = authorData.find((a) => a.id === message.authorId); return { ...message, author }; }); return new Response(JSON.stringify({ ticket, messages: newMessages }), { headers: { "Content-Type": "application/json" } }); }, DELETE: async (req: BunRequest) => { const user = await getSessionUser(req); if (!user) return new Response("Unauthorized", { status: 401 }); const modCheck = await db.select().from(moderators).where(eq(moderators.user_id, user.id)).limit(1); if (modCheck.length === 0) return new Response("Forbidden: You must be a moderator to delete tickets.", { status: 403 }); const { id } = req.params; const ticketId = parseInt(id, 10); if (isNaN(ticketId)) return new Response("Invalid ID", { status: 400 }); const deleted = await db.delete(tickets).where(eq(tickets.id, ticketId)).returning(); if (deleted.length === 0) return new Response("Ticket not found", { status: 404 }); return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } }); } }, "/moderators": { GET: async (req: BunRequest) => { const user = await getSessionUser(req); if (!user) return new Response("Unauthorized", { status: 401 }); const modCheck = await db.select().from(moderators).where(eq(moderators.user_id, user.id)).limit(1); if (modCheck.length === 0) return new Response("Forbidden", { status: 403 }); const mods = await db.select().from(moderators); const ids = mods.map(mod => mod.user_id); const moderatorUsers = await Promise.all(ids.map(id => getUser(id))); const hydrated = mods.map((mod, index) => ({ ...moderatorUsers[index], pronouns: mod })); return new Response(JSON.stringify(hydrated), { headers: { "Content-Type": "application/json" } }); } } }, }); const PORT = process.env.PORT || 3000; logger.info(`server is running at ${server.hostname}:${PORT}`);