diff --git a/server/src/index.ts b/server/src/index.ts index fcd6cb8..6c8803a 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -8,6 +8,9 @@ import { handleDashboardClose, } from "./ws/dashboard.js"; import type { WebSocketData } from "./ws/sessions.js"; +import { devices } from "./routes/devices.js"; +import { goals } from "./routes/goals.js"; +import { health } from "./routes/health.js"; const app = new Hono(); @@ -27,8 +30,10 @@ app.on(["POST", "GET"], "/api/auth/*", (c) => { return auth.handler(c.req.raw); }); -// Health check -app.get("/health", (c) => c.json({ status: "ok" })); +// REST routes +app.route("/devices", devices); +app.route("/goals", goals); +app.route("/health", health); // Start server with WebSocket support const server = Bun.serve({ diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts new file mode 100644 index 0000000..078c06c --- /dev/null +++ b/server/src/middleware/auth.ts @@ -0,0 +1,24 @@ +import type { Context, Next } from "hono"; +import { auth } from "../auth.js"; + +/** Hono Env type for routes protected by sessionMiddleware */ +export type AuthEnv = { + Variables: { + user: { id: string; name: string; email: string; [key: string]: unknown }; + session: { id: string; userId: string; [key: string]: unknown }; + }; +}; + +export async function sessionMiddleware(c: Context, next: Next) { + const session = await auth.api.getSession({ + headers: c.req.raw.headers, + }); + + if (!session) { + return c.json({ error: "unauthorized" }, 401); + } + + c.set("user", session.user); + c.set("session", session.session); + await next(); +} diff --git a/server/src/routes/devices.ts b/server/src/routes/devices.ts new file mode 100644 index 0000000..272abe5 --- /dev/null +++ b/server/src/routes/devices.ts @@ -0,0 +1,22 @@ +import { Hono } from "hono"; +import { sessionMiddleware, type AuthEnv } from "../middleware/auth.js"; +import { sessions } from "../ws/sessions.js"; + +const devices = new Hono(); +devices.use("*", sessionMiddleware); + +devices.get("/", (c) => { + const user = c.get("user"); + const userDevices = sessions.getDevicesForUser(user.id); + + return c.json( + userDevices.map((d) => ({ + deviceId: d.deviceId, + name: d.deviceInfo?.model ?? "Unknown Device", + deviceInfo: d.deviceInfo, + connectedAt: d.connectedAt.toISOString(), + })) + ); +}); + +export { devices }; diff --git a/server/src/routes/goals.ts b/server/src/routes/goals.ts new file mode 100644 index 0000000..4a392f1 --- /dev/null +++ b/server/src/routes/goals.ts @@ -0,0 +1,36 @@ +import { Hono } from "hono"; +import { sessionMiddleware, type AuthEnv } from "../middleware/auth.js"; +import { sessions } from "../ws/sessions.js"; + +const goals = new Hono(); +goals.use("*", sessionMiddleware); + +goals.post("/", async (c) => { + const user = c.get("user"); + const body = await c.req.json<{ deviceId: string; goal: string }>(); + + if (!body.deviceId || !body.goal) { + return c.json({ error: "deviceId and goal are required" }, 400); + } + + const device = sessions.getDevice(body.deviceId); + if (!device) { + return c.json({ error: "device not connected" }, 404); + } + + if (device.userId !== user.id) { + return c.json({ error: "device does not belong to you" }, 403); + } + + // TODO (Task 6): start agent loop for this device+goal + const sessionId = crypto.randomUUID(); + + return c.json({ + sessionId, + deviceId: body.deviceId, + goal: body.goal, + status: "queued", + }); +}); + +export { goals }; diff --git a/server/src/routes/health.ts b/server/src/routes/health.ts new file mode 100644 index 0000000..ca2b1df --- /dev/null +++ b/server/src/routes/health.ts @@ -0,0 +1,13 @@ +import { Hono } from "hono"; +import { sessions } from "../ws/sessions.js"; + +const health = new Hono(); + +health.get("/", (c) => { + return c.json({ + status: "ok", + connectedDevices: sessions.getStats().devices, + }); +}); + +export { health };