reworking router somewhat
This commit is contained in:
@ -3,12 +3,15 @@ import { createValidationError, z } from "@shared/utils/validator.ts";
|
||||
import {
|
||||
adminPasswordAlreadySetErrorSchema,
|
||||
adminPasswordNotSetErrorSchema,
|
||||
commandExecutionErrorSchema,
|
||||
failedToParseRequestAsJSONErrorSchema,
|
||||
invalidPasswordErrorSchema,
|
||||
passwordsMustMatchErrorSchema,
|
||||
queryExecutionErrorSchema,
|
||||
requestValidationErrorSchema,
|
||||
tooManyRequestsErrorSchema,
|
||||
unauthorizedErrorSchema,
|
||||
usbipUnknownErrorSchema,
|
||||
} from "@src/lib/errors.ts";
|
||||
|
||||
const loginApiSchema = {
|
||||
@ -66,3 +69,42 @@ export const passwordSetupApi = new Api(
|
||||
"POST",
|
||||
passwordSetupApiSchema,
|
||||
);
|
||||
|
||||
const updateDevicesApiSchema = {
|
||||
req: z.void(),
|
||||
res: z.result(
|
||||
z.void(),
|
||||
z.union([
|
||||
queryExecutionErrorSchema,
|
||||
tooManyRequestsErrorSchema,
|
||||
unauthorizedErrorSchema,
|
||||
commandExecutionErrorSchema,
|
||||
usbipUnknownErrorSchema,
|
||||
]),
|
||||
),
|
||||
};
|
||||
|
||||
export const updateDevicesApi = new Api(
|
||||
"/api/updateDevices",
|
||||
"POST",
|
||||
updateDevicesApiSchema,
|
||||
);
|
||||
|
||||
const versionApiSchema = {
|
||||
req: z.void(),
|
||||
res: z.result(
|
||||
z.obj({
|
||||
app: z.literal("Keyborg"),
|
||||
version: z.string(),
|
||||
}),
|
||||
z.union([
|
||||
tooManyRequestsErrorSchema,
|
||||
]),
|
||||
),
|
||||
};
|
||||
|
||||
export const versionApi = new Api(
|
||||
"/version",
|
||||
"POST",
|
||||
versionApiSchema,
|
||||
);
|
||||
|
||||
154
server/main.ts
154
server/main.ts
@ -4,8 +4,12 @@ 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 { SchemaValidationError, z } from "@shared/utils/validator.ts";
|
||||
import { loginApi, passwordSetupApi } from "./api.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";
|
||||
@ -20,6 +24,7 @@ import {
|
||||
import devices from "@src/lib/devices.ts";
|
||||
|
||||
const AUTH_COOKIE_NAME = "token";
|
||||
const VERSION = "0.1.0";
|
||||
|
||||
const router = new HttpRouter();
|
||||
|
||||
@ -50,9 +55,9 @@ router.get("/public/*", async (c) => {
|
||||
|
||||
router
|
||||
.get(["", "/index.html"], (c) => {
|
||||
console.log(devices.list());
|
||||
const devicesList = devices.list().unwrap().unwrap();
|
||||
|
||||
return c.html(eta.render("./index.html", {}));
|
||||
return c.html(eta.render("./index.html", { devices: devicesList }));
|
||||
})
|
||||
.get(["/login", "/login.html"], (c) => {
|
||||
const isSet = admin.isPasswordSet();
|
||||
@ -88,63 +93,104 @@ router
|
||||
);
|
||||
});
|
||||
|
||||
router.api(loginApi, async (c) => {
|
||||
const r = await c
|
||||
.parseBody()
|
||||
.andThenAsync(
|
||||
({ password }) => admin.verifyPassword(password),
|
||||
);
|
||||
router.get("ws", (c) => {
|
||||
if (c.req.headers.get("upgrade") != "websocket") {
|
||||
return new Response(null, { status: 501 });
|
||||
}
|
||||
|
||||
if (r.isErr()) {
|
||||
if (r.error.type === "AdminPasswordNotSetError") {
|
||||
return c.json400(
|
||||
err({
|
||||
type: r.error.type,
|
||||
info: r.error.info,
|
||||
}),
|
||||
const { socket, response } = Deno.upgradeWebSocket(c.req);
|
||||
|
||||
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("pinged!");
|
||||
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")),
|
||||
);
|
||||
}
|
||||
return handleCommonErrors(c, r.error);
|
||||
}
|
||||
})
|
||||
.api(passwordSetupApi, async (c) => {
|
||||
const r = await c.parseBody();
|
||||
|
||||
const isMatch = r.value;
|
||||
if (r.isErr()) {
|
||||
return handleCommonErrors(c, r.error);
|
||||
}
|
||||
|
||||
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),
|
||||
const v = r.value;
|
||||
|
||||
if (v.password !== v.passwordRepeat) {
|
||||
return c.json400(
|
||||
err(passwordsMustMatchError("Passwords must match")),
|
||||
);
|
||||
} else {
|
||||
return c.json(err(invalidPasswordError("Invalid login or password")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
router.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)),
|
||||
);
|
||||
});
|
||||
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<any, any, any>,
|
||||
|
||||
@ -0,0 +1 @@
|
||||
class c{ws=null;url;reconnectInterval;maxReconnectInterval;reconnectDecay;timeout;forcedClose=!1;onmessage;constructor(e,t={}){this.url=e,this.reconnectInterval=t.reconnectInterval??1e3,this.maxReconnectInterval=t.maxReconnectInterval??3e4,this.reconnectDecay=t.reconnectDecay??1.5,this.timeout=t.timeout??2e3,this.connect()}connect(e=!1){console.log(`Connecting to ${this.url}...`),this.ws=new WebSocket(this.url);let t=setTimeout(()=>{console.warn("Connection timeout, closing socket."),this.ws?.close()},this.timeout);this.ws.onopen=n=>{clearTimeout(t),console.log("WebSocket connected."),this.onmessage&&this.ws?.addEventListener("message",this.onmessage)},this.ws.onerror=n=>{console.error("WebSocket error:",n)},this.ws.onclose=n=>{clearTimeout(t),console.log("WebSocket closed:",n.reason),this.forcedClose||setTimeout(()=>{this.reconnectInterval=Math.min(this.reconnectInterval*this.reconnectDecay,this.maxReconnectInterval),this.connect(!0)},this.reconnectInterval)}}onMessage(e){this.ws&&this.ws.addEventListener("message",e),this.onmessage=e}send(e){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(e):console.error("WebSocket is not open. Message not sent.")}close(){this.forcedClose=!0,this.ws?.close()}}const s=new c("/ws");s.onMessage(o=>{console.log(o.data)});const i=document.getElementById("ping");i.onclick=()=>{s.send("ping")};
|
||||
File diff suppressed because one or more lines are too long
@ -1 +1,103 @@
|
||||
interface ReconnectOptions {
|
||||
reconnectInterval?: number; // Initial reconnect delay (ms)
|
||||
maxReconnectInterval?: number; // Maximum delay (ms)
|
||||
reconnectDecay?: number; // Exponential backoff multiplier
|
||||
timeout?: number; // Connection timeout (ms)
|
||||
}
|
||||
|
||||
class ReconnectingWebSocketClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private url: string;
|
||||
private reconnectInterval: number;
|
||||
private maxReconnectInterval: number;
|
||||
private reconnectDecay: number;
|
||||
private timeout: number;
|
||||
private forcedClose: boolean = false;
|
||||
private onmessage?: (ev: MessageEvent) => any;
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
options: ReconnectOptions = {},
|
||||
) {
|
||||
this.url = url;
|
||||
this.reconnectInterval = options.reconnectInterval ?? 1000; // 1 second
|
||||
this.maxReconnectInterval = options.maxReconnectInterval ?? 30000; // 30 seconds
|
||||
this.reconnectDecay = options.reconnectDecay ?? 1.5;
|
||||
this.timeout = options.timeout ?? 2000; // 2 seconds
|
||||
this.connect();
|
||||
}
|
||||
|
||||
private connect(isReconnect: boolean = false): void {
|
||||
console.log(`Connecting to ${this.url}...`);
|
||||
|
||||
this.ws = new WebSocket(this.url);
|
||||
let connectionTimeout = setTimeout(() => {
|
||||
console.warn("Connection timeout, closing socket.");
|
||||
this.ws?.close();
|
||||
}, this.timeout);
|
||||
|
||||
this.ws.onopen = (event: Event) => {
|
||||
clearTimeout(connectionTimeout);
|
||||
console.log("WebSocket connected.");
|
||||
|
||||
if (this.onmessage) {
|
||||
this.ws?.addEventListener("message", this.onmessage);
|
||||
}
|
||||
|
||||
// On connection, send login credentials
|
||||
// Optionally, if this is a reconnection, you could dispatch a custom event or handle state changes.
|
||||
};
|
||||
|
||||
this.ws.onerror = (event: Event) => {
|
||||
console.error("WebSocket error:", event);
|
||||
};
|
||||
|
||||
this.ws.onclose = (event: CloseEvent) => {
|
||||
clearTimeout(connectionTimeout);
|
||||
console.log("WebSocket closed:", event.reason);
|
||||
if (!this.forcedClose) {
|
||||
// Schedule reconnection with exponential backoff
|
||||
setTimeout(() => {
|
||||
this.reconnectInterval = Math.min(
|
||||
this.reconnectInterval * this.reconnectDecay,
|
||||
this.maxReconnectInterval,
|
||||
);
|
||||
this.connect(true);
|
||||
}, this.reconnectInterval);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public onMessage(fn: (e: MessageEvent) => void) {
|
||||
if (this.ws) {
|
||||
this.ws.addEventListener("message", fn);
|
||||
}
|
||||
|
||||
this.onmessage = fn;
|
||||
}
|
||||
|
||||
public send(data: any): void {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(data);
|
||||
} else {
|
||||
console.error("WebSocket is not open. Message not sent.");
|
||||
}
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.forcedClose = true;
|
||||
this.ws?.close();
|
||||
}
|
||||
}
|
||||
|
||||
const ws = new ReconnectingWebSocketClient("/ws");
|
||||
|
||||
ws.onMessage((e) => {
|
||||
console.log(e.data);
|
||||
});
|
||||
|
||||
const pingBtn = document.getElementById("ping") as HTMLButtonElement;
|
||||
|
||||
pingBtn.onclick = () => {
|
||||
ws.send("ping");
|
||||
};
|
||||
|
||||
@ -65,6 +65,8 @@ export class Api<
|
||||
}
|
||||
const path = pathSplitted.join("/");
|
||||
|
||||
console.log(data);
|
||||
|
||||
const response = await fetch(
|
||||
path,
|
||||
{
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import usbip, {
|
||||
CommandExecutionError,
|
||||
DeviceDetailed,
|
||||
DeviceDoesNotExistError,
|
||||
deviceDoesNotExistError,
|
||||
UsbipUnknownError,
|
||||
} from "@src/lib/usbip.ts";
|
||||
import usbip, { DeviceDetailed } from "@src/lib/usbip.ts";
|
||||
import { none, Option, some } from "@shared/utils/option.ts";
|
||||
import { InferSchemaType, z } from "@shared/utils/validator.ts";
|
||||
import log from "@shared/utils/logger.ts";
|
||||
import { ResultAsync } from "@shared/utils/resultasync.ts";
|
||||
import { err, Ok, ok, Result } from "@shared/utils/result.ts";
|
||||
import {
|
||||
CommandExecutionError,
|
||||
DeviceDoesNotExistError,
|
||||
deviceDoesNotExistError,
|
||||
UsbipUnknownError,
|
||||
} from "@src/lib/errors.ts";
|
||||
|
||||
type FailedToAccessDevices = CommandExecutionError | UsbipUnknownError;
|
||||
|
||||
|
||||
@ -116,3 +116,51 @@ export const passwordsMustMatchError = createErrorFactory(
|
||||
export type PasswordsMustMatchError = InferSchemaType<
|
||||
typeof passwordsMustMatchErrorSchema
|
||||
>;
|
||||
|
||||
export const commandExecutionErrorSchema = defineError("CommandExecutionError");
|
||||
export const commandExecutionError = createErrorFactory(
|
||||
commandExecutionErrorSchema,
|
||||
);
|
||||
export type CommandExecutionError = InferSchemaType<
|
||||
typeof commandExecutionErrorSchema
|
||||
>;
|
||||
|
||||
export const deviceDoesNotExistErrorSchema = defineError(
|
||||
"DeviceDoesNotExistError",
|
||||
);
|
||||
export const deviceDoesNotExistError = createErrorFactory(
|
||||
deviceDoesNotExistErrorSchema,
|
||||
);
|
||||
export type DeviceDoesNotExistError = InferSchemaType<
|
||||
typeof deviceDoesNotExistErrorSchema
|
||||
>;
|
||||
|
||||
export const deviceAlreadyBoundErrorSchema = defineError(
|
||||
"DeviceAlreadyBoundError",
|
||||
);
|
||||
export const deviceAlreadyBoundError = createErrorFactory(
|
||||
deviceAlreadyBoundErrorSchema,
|
||||
);
|
||||
export type DeviceAlreadyBoundError = InferSchemaType<
|
||||
typeof deviceAlreadyBoundErrorSchema
|
||||
>;
|
||||
|
||||
export const deviceNotBoundErrorSchema = defineError("DeviceNotBoundError");
|
||||
export const deviceNotBoundError = createErrorFactory(
|
||||
deviceNotBoundErrorSchema,
|
||||
);
|
||||
export type DeviceNotBoundError = InferSchemaType<
|
||||
typeof deviceNotBoundErrorSchema
|
||||
>;
|
||||
|
||||
export const usbipUnknownErrorSchema = defineError("UsbipUnknownError");
|
||||
export const usbipUnknownError = createErrorFactory(usbipUnknownErrorSchema);
|
||||
export type UsbipUnknownError = InferSchemaType<typeof usbipUnknownErrorSchema>;
|
||||
|
||||
export const notFoundErrorSchema = defineError("NotFoundError");
|
||||
export const notFoundError = createErrorFactory(notFoundErrorSchema);
|
||||
export type NotFoundError = InferSchemaType<typeof notFoundErrorSchema>;
|
||||
|
||||
export const notAllowedErrorSchema = defineError("NotAllowedError");
|
||||
export const notAllowedError = createErrorFactory(notAllowedErrorSchema);
|
||||
export type NotAllowedError = InferSchemaType<typeof notAllowedErrorSchema>;
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { RouterTree } from "@lib/routerTree.ts";
|
||||
import { none, Option, some } from "@shared/utils/option.ts";
|
||||
import { Context } from "@lib/context.ts";
|
||||
import { RouterTree } from "@src/lib/routerTree.ts";
|
||||
import { none, some } from "@shared/utils/option.ts";
|
||||
import { Context } from "@src/lib/context.ts";
|
||||
import { Schema } from "@shared/utils/validator.ts";
|
||||
import { Api } from "@src/lib/apiValidator.ts";
|
||||
import { notAllowedError, notFoundError } from "@src/lib/errors.ts";
|
||||
import { err } from "@shared/utils/result.ts";
|
||||
|
||||
type RequestHandler<
|
||||
S extends string,
|
||||
@ -10,44 +12,39 @@ type RequestHandler<
|
||||
ResSchema extends Schema<any> = Schema<unknown>,
|
||||
> = (c: Context<S, ReqSchema, ResSchema>) => Promise<Response> | Response;
|
||||
|
||||
type RequestHandlerWithSchema<S extends string> = {
|
||||
handler: RequestHandler<S>;
|
||||
schema?: {
|
||||
res: Schema<any>;
|
||||
req: Schema<any>;
|
||||
};
|
||||
};
|
||||
export type Middleware = (
|
||||
c: Context<string>,
|
||||
next: () => Promise<void>,
|
||||
) => Promise<Response | void> | Response | void;
|
||||
|
||||
type MethodHandler<S extends string> = {
|
||||
handler: RequestHandler<S>;
|
||||
schema?: { req: Schema<any>; res: Schema<any> };
|
||||
};
|
||||
|
||||
type MethodHandlers<S extends string> = Partial<
|
||||
Record<string, {
|
||||
handler: RequestHandler<S>;
|
||||
schema?: {
|
||||
res: Schema<any>;
|
||||
req: Schema<any>;
|
||||
};
|
||||
}>
|
||||
Record<string, MethodHandler<S>>
|
||||
>;
|
||||
|
||||
const DEFAULT_NOT_FOUND_HANDLER = () => new Response("404 Not found");
|
||||
const DEFAULT_NOT_FOUND_HANDLER =
|
||||
(() => new Response("404 Not found", { status: 404 })) as RequestHandler<
|
||||
any
|
||||
>;
|
||||
|
||||
class HttpRouter {
|
||||
public readonly routerTree = new RouterTree<MethodHandlers<any>>();
|
||||
public pathPreprocessor?: (path: string) => string;
|
||||
public pathTransformer?: (path: string) => string;
|
||||
private middlewares: Middleware[] = [];
|
||||
public defaultNotFoundHandler: RequestHandler<string> =
|
||||
DEFAULT_NOT_FOUND_HANDLER;
|
||||
|
||||
public setPathProcessor(processor: (path: string) => string) {
|
||||
this.pathPreprocessor = processor;
|
||||
public setPathTransformer(transformer: (path: string) => string) {
|
||||
this.pathTransformer = transformer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public use(mw: Middleware): this {
|
||||
this.middlewares.push(mw);
|
||||
public use(middleware: Middleware): this {
|
||||
this.middlewares.push(middleware);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -61,7 +58,6 @@ class HttpRouter {
|
||||
handler: RequestHandler<S, ReqSchema, ResSchema>,
|
||||
schema?: { req: ReqSchema; res: ResSchema },
|
||||
): HttpRouter;
|
||||
|
||||
public add<
|
||||
S extends string,
|
||||
ReqSchema extends Schema<any> = Schema<unknown>,
|
||||
@ -72,7 +68,6 @@ class HttpRouter {
|
||||
handler: RequestHandler<string, ReqSchema, ResSchema>,
|
||||
schema?: { req: ReqSchema; res: ResSchema },
|
||||
): HttpRouter;
|
||||
|
||||
public add(
|
||||
path: string | string[],
|
||||
method: string,
|
||||
@ -83,13 +78,13 @@ class HttpRouter {
|
||||
|
||||
for (const p of paths) {
|
||||
this.routerTree.getHandler(p).match(
|
||||
(mth) => {
|
||||
mth[method] = { handler, schema };
|
||||
(existingHandlers) => {
|
||||
existingHandlers[method] = { handler, schema };
|
||||
},
|
||||
() => {
|
||||
const mth: MethodHandlers<string> = {};
|
||||
mth[method] = { handler, schema };
|
||||
this.routerTree.add(p, mth);
|
||||
const newHandlers: MethodHandlers<string> = {};
|
||||
newHandlers[method] = { handler, schema };
|
||||
this.routerTree.add(p, newHandlers);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -149,19 +144,46 @@ class HttpRouter {
|
||||
connInfo: Deno.ServeHandlerInfo<Deno.Addr>,
|
||||
): Promise<Response> {
|
||||
let ctx = new Context(req, connInfo, {});
|
||||
const path = this.pathTransformer
|
||||
? this.pathTransformer(ctx.path)
|
||||
: ctx.path;
|
||||
|
||||
let routeParams: Record<string, string> = {};
|
||||
const path = this.pathPreprocessor
|
||||
? this.pathPreprocessor(ctx.path)
|
||||
: ctx.path;
|
||||
|
||||
const handler = this.routerTree
|
||||
.find(path)
|
||||
.andThen((match) => {
|
||||
const { value: methodHandler, params: params } = match;
|
||||
routeParams = params;
|
||||
const route = methodHandler[req.method];
|
||||
if (!route) return none;
|
||||
|
||||
let route = methodHandler[req.method];
|
||||
|
||||
if (!route) {
|
||||
if (req.method === "HEAD") {
|
||||
const getHandler = methodHandler["GET"];
|
||||
if (!getHandler) {
|
||||
return none;
|
||||
}
|
||||
route = getHandler;
|
||||
} else if (
|
||||
ctx.preferredType.map((v) => v === "json")
|
||||
.toBoolean() &&
|
||||
req.method !== "GET"
|
||||
) {
|
||||
return some(
|
||||
(() =>
|
||||
ctx.json(
|
||||
err(notAllowedError(
|
||||
"405 Not allowed",
|
||||
)),
|
||||
{
|
||||
status: 405,
|
||||
},
|
||||
)) as RequestHandler<any>,
|
||||
);
|
||||
}
|
||||
return none;
|
||||
}
|
||||
if (route.schema) {
|
||||
ctx = ctx.setSchema(route.schema);
|
||||
}
|
||||
@ -169,7 +191,22 @@ class HttpRouter {
|
||||
|
||||
return some(handler);
|
||||
})
|
||||
.unwrapOrElse(() => this.defaultNotFoundHandler);
|
||||
.unwrapOrElse(() => {
|
||||
switch (ctx.preferredType.unwrapOr("other")) {
|
||||
case "json":
|
||||
return (() =>
|
||||
ctx.json(err(notFoundError("404 Not found")), {
|
||||
status: 404,
|
||||
})) as RequestHandler<any>;
|
||||
case "html":
|
||||
return (() =>
|
||||
ctx.html("404 Not found", {
|
||||
status: 404,
|
||||
})) as RequestHandler<any>;
|
||||
case "other":
|
||||
return DEFAULT_NOT_FOUND_HANDLER;
|
||||
}
|
||||
});
|
||||
|
||||
const res = (await this.executeMiddlewareChain(
|
||||
this.middlewares,
|
||||
@ -177,9 +214,27 @@ class HttpRouter {
|
||||
ctx = ctx.setParams(routeParams),
|
||||
)).res;
|
||||
|
||||
if (req.method === "HEAD") {
|
||||
const headers = new Headers(res.headers);
|
||||
headers.set("Content-Length", "0");
|
||||
return new Response(null, {
|
||||
headers,
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private resolveRoute(
|
||||
ctx: Context,
|
||||
req: Request,
|
||||
path: string,
|
||||
): { handler: RequestHandler<any>; params: Record<string, string> } {
|
||||
const routeOption = this.routerTree.find(path);
|
||||
}
|
||||
|
||||
private async executeMiddlewareChain<S extends string>(
|
||||
middlewares: Middleware[],
|
||||
handler: RequestHandler<S>,
|
||||
|
||||
@ -8,48 +8,18 @@ import {
|
||||
type Option,
|
||||
some,
|
||||
} from "@shared/utils/option.ts";
|
||||
import { createErrorFactory, defineError } from "@shared/utils/errors.ts";
|
||||
import { InferSchemaType } from "@shared/utils/validator.ts";
|
||||
|
||||
export const commandExecutionErrorSchema = defineError("CommandExecutionError");
|
||||
export const commandExecutionError = createErrorFactory(
|
||||
commandExecutionErrorSchema,
|
||||
);
|
||||
export type CommandExecutionError = InferSchemaType<
|
||||
typeof commandExecutionErrorSchema
|
||||
>;
|
||||
|
||||
export const deviceDoesNotExistErrorSchema = defineError(
|
||||
"DeviceDoesNotExistError",
|
||||
);
|
||||
export const deviceDoesNotExistError = createErrorFactory(
|
||||
deviceDoesNotExistErrorSchema,
|
||||
);
|
||||
export type DeviceDoesNotExistError = InferSchemaType<
|
||||
typeof deviceDoesNotExistErrorSchema
|
||||
>;
|
||||
|
||||
export const deviceAlreadyBoundErrorSchema = defineError(
|
||||
"DeviceAlreadyBoundError",
|
||||
);
|
||||
export const deviceAlreadyBoundError = createErrorFactory(
|
||||
deviceAlreadyBoundErrorSchema,
|
||||
);
|
||||
export type DeviceAlreadyBoundError = InferSchemaType<
|
||||
typeof deviceAlreadyBoundErrorSchema
|
||||
>;
|
||||
|
||||
export const deviceNotBoundErrorSchema = defineError("DeviceNotBoundError");
|
||||
export const deviceNotBoundError = createErrorFactory(
|
||||
deviceNotBoundErrorSchema,
|
||||
);
|
||||
export type DeviceNotBoundError = InferSchemaType<
|
||||
typeof deviceNotBoundErrorSchema
|
||||
>;
|
||||
|
||||
export const usbipUnknownErrorSchema = defineError("UsbipUnknownError");
|
||||
export const usbipUnknownError = createErrorFactory(usbipUnknownErrorSchema);
|
||||
export type UsbipUnknownError = InferSchemaType<typeof usbipUnknownErrorSchema>;
|
||||
import {
|
||||
CommandExecutionError,
|
||||
commandExecutionError,
|
||||
DeviceAlreadyBoundError,
|
||||
deviceAlreadyBoundError,
|
||||
DeviceDoesNotExistError,
|
||||
deviceDoesNotExistError,
|
||||
DeviceNotBoundError,
|
||||
deviceNotBoundError,
|
||||
UsbipUnknownError,
|
||||
usbipUnknownError,
|
||||
} from "@src/lib/errors.ts";
|
||||
|
||||
type UsbipCommonError = DeviceDoesNotExistError | UsbipUnknownError;
|
||||
|
||||
|
||||
@ -8,8 +8,7 @@ import {
|
||||
import { err, ok } from "@shared/utils/result.ts";
|
||||
import { eta } from "../../main.ts";
|
||||
|
||||
const LOGIN_PATH = "/login";
|
||||
const SETUP_PATH = "/setup";
|
||||
const EXCLUDE = new Set(["/login", "/setup", "/version"]);
|
||||
|
||||
const authMiddleware: Middleware = async (c, next) => {
|
||||
const token = c.cookies.get("token");
|
||||
@ -33,8 +32,7 @@ const authMiddleware: Middleware = async (c, next) => {
|
||||
const path = c.path;
|
||||
|
||||
if (
|
||||
!isValid.value && !path.startsWith("/public") && path !== LOGIN_PATH &&
|
||||
path !== SETUP_PATH
|
||||
!isValid.value && !path.startsWith("/public") && !EXCLUDE.has(path)
|
||||
) {
|
||||
if (!isValid.value) {
|
||||
c.cookies.delete("token");
|
||||
|
||||
@ -1,3 +1,13 @@
|
||||
<% layout("./layouts/layout.html") %>
|
||||
devices:
|
||||
<% it.devices.forEach(function(device){ %>
|
||||
<div>
|
||||
name: <%= device.name %> | <%= device.vendor %>
|
||||
busid: <%= device.busid %>
|
||||
</div>
|
||||
<%= device.busid %>
|
||||
<% }) %>
|
||||
|
||||
<script src="/public/js/index.js" defer></script>
|
||||
<button id="ping">ping</button>
|
||||
|
||||
<script src="/public/js/index.js" defer></script>
|
||||
|
||||
BIN
server/test.db
BIN
server/test.db
Binary file not shown.
@ -1,6 +1 @@
|
||||
<% layout("./layouts/layout.html") %>
|
||||
|
||||
devices:
|
||||
<div id="Devices"></div>
|
||||
|
||||
<script defer src=/public/js/index.js></script>
|
||||
<% layout("./layouts/layout.html") %> devices: <% it.devices.forEach(function(device){ %> <div>name: <%= device.name %> | <%= device.vendor %> busid: <%= device.busid %></div> <%= device.busid %> <% }) %> <button id=ping>ping</button><script defer src=/public/js/index.js></script>
|
||||
Reference in New Issue
Block a user