import HttpRouter from "@src/lib/router.ts"; import { Eta } from "@eta-dev/eta"; import { serveFile } from "jsr:@std/http/file-server"; import rateLimitMiddleware from "@src/middleware/rateLimiter.ts"; import authMiddleware from "@src/middleware/auth.ts"; import loggerMiddleware from "@src/middleware/logger.ts"; import { loginApi, passwordSetupApi, updateDevicesApi, versionApi, } from "./api.ts"; import { err, ok } from "@shared/utils/result.ts"; import admin from "@src/lib/admin.ts"; import { Context } from "@src/lib/context.ts"; import { FailedToParseRequestAsJSONError, invalidPasswordError, passwordsMustMatchError, QueryExecutionError, queryExecutionError, RequestValidationError, } from "@src/lib/errors.ts"; import devices from "@src/lib/devices.ts"; import { WebSocketClientsGroup } from "@src/lib/websocket.ts"; const AUTH_COOKIE_NAME = "token"; const VERSION = "0.1.0-a.1"; const router = new HttpRouter(); const views = Deno.cwd() + "/views/"; export const eta = new Eta({ views }); router.use(loggerMiddleware); router.use(rateLimitMiddleware); router.use(authMiddleware); const cache: Map = new Map(); router.get("/public/*", async (c) => { const filePath = "." + c.path; //const cached = cache.get(filePath); // //if (cached) { // return cached.clone(); //} const res = await serveFile(c.req, filePath); //cache.set(filePath, res.clone()); return res; }); router .get(["", "/index.html"], (c) => { const devicesList = devices.list().unwrap().unwrap(); return c.html(eta.render("./index.html", { devices: devicesList })); }) .get(["/login", "/login.html"], (c) => { const isSet = admin.isPasswordSet(); if (isSet.isErr()) { return c.html(eta.render("./internal_error.html", {})); } if (!isSet.value) { return c.redirect("/setup"); } const alreadyLoggedIn = c.cookies.get("token").map((token) => admin.sessions.verifyToken(token) ) .toBoolean(); return c.html(eta.render("./login.html", { alreadyLoggedIn })); }) .get("/setup", (c) => { return admin.isPasswordSet() .match( (isSet) => { if (isSet) { return c.redirect("/login"); } else { return c.html(eta.render("./setup.html", {})); } }, (e) => c.html(eta.render("./internal_error.html", {})), ); }); const group = new WebSocketClientsGroup(); router.get("/api/admin/ws", (c) => { if (c.req.headers.get("upgrade") != "websocket") { return new Response(null, { status: 501 }); } const { socket, response } = Deno.upgradeWebSocket(c.req); group.addClient(socket); socket.addEventListener("open", () => { console.log("a client connected!"); }); socket.addEventListener("close", () => { console.log("client disconnected"); }); socket.addEventListener("message", (event) => { if (event.data === "ping") { console.log("ping"); socket.send("pong"); } }); return response; }); router .api(loginApi, async (c) => { const r = await c .parseBody() .andThenAsync( ({ password }) => admin.verifyPassword(password), ); if (r.isErr()) { if (r.error.type === "AdminPasswordNotSetError") { return c.json400( err({ type: r.error.type, info: r.error.info, }), ); } return handleCommonErrors(c, r.error); } const isMatch = r.value; if (isMatch) { return admin.sessions.create() .map(({ value, expires }) => { c.cookies.set({ name: AUTH_COOKIE_NAME, value, expires, }); return ok(); }).match( (v) => c.json(v), (e) => handleCommonErrors(c, e), ); } else { return c.json( err(invalidPasswordError("Invalid login or password")), ); } }) .api(passwordSetupApi, async (c) => { const r = await c.parseBody(); if (r.isErr()) { return handleCommonErrors(c, r.error); } const v = r.value; if (v.password !== v.passwordRepeat) { return c.json400( err(passwordsMustMatchError("Passwords must match")), ); } return admin.setPassword(v.password).match( () => c.json(ok()), (e) => c.json400(err(e)), ); }) .api(updateDevicesApi, (c) => { return devices.updateDevices().match( () => c.json(ok()), (e) => c.json500(err(e)), ); }) .api(versionApi, (c) => { return c.json(ok({ app: "Keyborg", version: VERSION, })); }); function handleCommonErrors( c: Context, error: | QueryExecutionError | FailedToParseRequestAsJSONError | RequestValidationError, ): Response { switch (error.type) { case "QueryExecutionError": return c.json( err(queryExecutionError("Server failed to execute query")), { status: 500 }, ); case "FailedToParseRequestAsJSONError": return c.json( err(error), { status: 400 }, ); case "RequestValidationError": return c.json( err(error), { status: 400 }, ); } } export default { async fetch(req, connInfo) { return await router.handleRequest(req, connInfo); }, } satisfies Deno.ServeDefaultExport;