Keyborg/server/main.ts
2025-03-19 20:15:48 +03:00

229 lines
6.0 KiB
TypeScript

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<string, Response> = 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;