225 lines
5.9 KiB
TypeScript
225 lines
5.9 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";
|
|
import { Option } from "@shared/utils/option.ts";
|
|
|
|
const AUTH_COOKIE_NAME = "token";
|
|
const VERSION = "0.1.0-a.1";
|
|
|
|
export type Variables = {
|
|
token: string;
|
|
};
|
|
|
|
const router = new HttpRouter<Variables>();
|
|
|
|
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();
|
|
group.onmessage = (e) => {
|
|
group.sendToAll("pong");
|
|
console.log("ping");
|
|
};
|
|
|
|
router.get("/api/admin/ws", (c) => {
|
|
if (c.req.headers.get("upgrade") != "websocket") {
|
|
return new Response(null, { status: 501 });
|
|
}
|
|
|
|
const token = c.var.get("token");
|
|
|
|
let { socket, response } = Deno.upgradeWebSocket(c.req);
|
|
|
|
socket = group.addClient(token, socket).unwrap();
|
|
|
|
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;
|