Files
hammer-queue/backend/src/index.ts
Hammer 819649c8c7 fix: chat WS relay URL + chat UX improvements
- Fix docker-compose to read GATEWAY_WS_URL (was VITE_WS_URL, never set)
- Fix gateway-relay.ts default to ws.hammer.donovankelly.xyz
- Fix Elysia TS errors in error handlers (cast to any)
- Add thinking/typing indicator in chat (bouncing dots)
- Add message timestamps (tap to show)
- Add thread renaming (double-click thread name)
- Auto-resize chat input textarea
2026-01-29 08:34:45 +00:00

159 lines
4.7 KiB
TypeScript

import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { taskRoutes } from "./routes/tasks";
import { adminRoutes } from "./routes/admin";
import { projectRoutes } from "./routes/projects";
import { chatRoutes } from "./routes/chat";
import { auth } from "./lib/auth";
import { db } from "./db";
import { tasks, users } from "./db/schema";
import { isNull, asc, sql, eq } from "drizzle-orm";
const PORT = process.env.PORT || 3100;
// Backfill task numbers for existing tasks that don't have one
async function backfillTaskNumbers() {
const unnumbered = await db
.select()
.from(tasks)
.where(isNull(tasks.taskNumber))
.orderBy(asc(tasks.createdAt));
if (unnumbered.length === 0) return;
const maxNum = await db
.select({ max: sql<number>`COALESCE(MAX(${tasks.taskNumber}), 0)` })
.from(tasks);
let next = (maxNum[0]?.max ?? 0) + 1;
for (const task of unnumbered) {
await db
.update(tasks)
.set({ taskNumber: next++ })
.where(sql`${tasks.id} = ${task.id}`);
}
console.log(`Backfilled ${unnumbered.length} task numbers (${next - unnumbered.length} to ${next - 1})`);
}
backfillTaskNumbers().catch(console.error);
// Ensure donovan@donovankelly.xyz is admin
async function ensureAdmin() {
const adminEmail = "donovan@donovankelly.xyz";
const result = await db
.update(users)
.set({ role: "admin" })
.where(eq(users.email, adminEmail))
.returning({ id: users.id, email: users.email, role: users.role });
if (result.length) {
console.log(`Admin role ensured for ${adminEmail}`);
}
}
ensureAdmin().catch(console.error);
const app = new Elysia()
.use(
cors({
origin: ["https://dash.donovankelly.xyz", "https://queue.donovankelly.xyz", "http://localhost:5173"],
credentials: true,
allowedHeaders: ["Content-Type", "Authorization", "Cookie"],
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
})
)
// Mount BetterAuth handler
.all("/api/auth/*", async ({ request }) => {
return auth.handler(request);
})
// Invite route - create a user (bearer token or session auth)
.post("/api/invite", async ({ request, headers, body }) => {
const bearerToken = process.env.API_BEARER_TOKEN || "hammer-dev-token";
const authHeader = headers["authorization"];
// Check bearer token first
let authorized = authHeader === `Bearer ${bearerToken}`;
// If no bearer token, check session
if (!authorized) {
const session = await auth.api.getSession({ headers: request.headers });
authorized = !!session;
}
if (!authorized) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const { email, password, name } = body as {
email: string;
password: string;
name: string;
};
if (!email || !password || !name) {
return new Response(
JSON.stringify({ error: "email, password, and name are required" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
try {
const user = await auth.api.signUpEmail({
body: { email, password, name },
});
return new Response(JSON.stringify({ success: true, user }), {
headers: { "Content-Type": "application/json" },
});
} catch (e: any) {
return new Response(
JSON.stringify({ error: e.message || "Failed to create user" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
})
.use(taskRoutes)
.use(projectRoutes)
.use(adminRoutes)
.use(chatRoutes)
// Current user info (role, etc.)
.get("/api/me", async ({ request }) => {
try {
const session = await auth.api.getSession({ headers: request.headers });
if (!session?.user) return { authenticated: false };
return {
authenticated: true,
user: {
id: session.user.id,
name: session.user.name,
email: session.user.email,
role: (session.user as any).role || "user",
},
};
} catch {
return { authenticated: false };
}
})
.get("/health", () => ({ status: "ok", service: "hammer-queue" }))
.onError(({ error, set }) => {
const msg = (error as any)?.message || String(error);
if (msg === "Unauthorized") {
set.status = 401;
return { error: "Unauthorized" };
}
if (msg === "Task not found") {
set.status = 404;
return { error: "Task not found" };
}
console.error("Unhandled error:", msg);
set.status = 500;
return { error: "Internal server error" };
})
.listen(PORT);
console.log(`🔨 Hammer Queue API running on port ${PORT}`);