/** * Web server for bot configuration */ import { Hono } from "hono"; import { cors } from "hono/cors"; import { config } from "../core/config"; import { createLogger } from "../core/logger"; import type { BotClient } from "../core/client"; import * as oauth from "./oauth"; import * as session from "./session"; import { createApiRoutes } from "./api"; const logger = createLogger("Web"); // Store for OAuth state tokens const pendingStates = new Map(); const STATE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes export function createWebServer(client: BotClient) { const app = new Hono(); // CORS for API requests app.use("/api/*", cors({ origin: config.web.baseUrl, credentials: true, })); // Health check app.get("/health", (c) => c.json({ status: "ok" })); // OAuth login redirect app.get("/auth/login", (c) => { const state = crypto.randomUUID(); pendingStates.set(state, { createdAt: Date.now() }); // Clean up old states const now = Date.now(); for (const [key, value] of pendingStates) { if (now - value.createdAt > STATE_EXPIRY_MS) { pendingStates.delete(key); } } return c.redirect(oauth.getAuthorizationUrl(state)); }); // OAuth callback app.get("/auth/callback", async (c) => { const code = c.req.query("code"); const state = c.req.query("state"); const error = c.req.query("error"); if (error) { return c.html(`

Authentication failed

${error}

`); } if (!code || !state) { return c.html("

Invalid callback

", 400); } // Verify state if (!pendingStates.has(state)) { return c.html("

Invalid or expired state

", 400); } pendingStates.delete(state); try { // Exchange code for tokens const tokens = await oauth.exchangeCode(code); // Get user info const user = await oauth.getUser(tokens.access_token); // Create session const sessionId = await session.createSession( user.id, tokens.access_token, tokens.refresh_token, tokens.expires_in ); session.setSessionCookie(c, sessionId); // Redirect to dashboard return c.redirect("/"); } catch (err) { logger.error("OAuth callback failed", err); return c.html("

Authentication failed

", 500); } }); // Logout app.post("/auth/logout", async (c) => { const sessionId = session.getSessionCookie(c); if (sessionId) { await session.deleteSession(sessionId); session.clearSessionCookie(c); } return c.json({ success: true }); }); // Get current user app.get("/auth/me", async (c) => { const sessionId = session.getSessionCookie(c); if (!sessionId) { return c.json({ authenticated: false }); } const sess = await session.getSession(sessionId); if (!sess) { session.clearSessionCookie(c); return c.json({ authenticated: false }); } try { const user = await oauth.getUser(sess.accessToken); return c.json({ authenticated: true, user: { id: user.id, username: user.username, global_name: user.global_name, avatar: oauth.getAvatarUrl(user), }, }); } catch { return c.json({ authenticated: false }); } }); // Mount API routes app.route("/api", createApiRoutes(client)); // Simple dashboard HTML app.get("/", async (c) => { const sessionId = session.getSessionCookie(c); const sess = sessionId ? await session.getSession(sessionId) : null; if (!sess) { return c.html(` Joel Bot Dashboard

Joel Bot Dashboard

Configure Joel's personalities and options for your servers.

Login with Discord `); } return c.html(` Joel Bot Dashboard
Loading...
`); }); return app; } export async function startWebServer(client: BotClient): Promise { const app = createWebServer(client); logger.info(`Starting web server on port ${config.web.port}`); Bun.serve({ port: config.web.port, fetch: app.fetch, }); logger.info(`Web server running at ${config.web.baseUrl}`); }