mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
Add new ServerHandler package which handles requests
This commit is contained in:
parent
3cdd685035
commit
327a716b12
45
Dockerfile.FE
Normal file
45
Dockerfile.FE
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Bun doesn't run well on Musl but this seems to work
|
||||
FROM imbios/bun-node:1.1.3-current-alpine as base
|
||||
|
||||
# Install dependencies into temp directory
|
||||
# This will cache them and speed up future builds
|
||||
FROM base AS install
|
||||
|
||||
RUN mkdir -p /temp
|
||||
COPY . /temp
|
||||
WORKDIR /temp
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
FROM base as build
|
||||
|
||||
# Copy the project
|
||||
RUN mkdir -p /temp
|
||||
COPY . /temp
|
||||
# Copy dependencies
|
||||
COPY --from=install /temp/node_modules /temp/node_modules
|
||||
# Build the project
|
||||
WORKDIR /temp
|
||||
RUN bun run prod-build
|
||||
WORKDIR /temp/dist
|
||||
|
||||
# Copy production dependencies and source code into final image
|
||||
FROM oven/bun:1.1.3-alpine
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir -p /app
|
||||
COPY --from=build /temp/dist /app/dist
|
||||
COPY entrypoint.sh /app
|
||||
|
||||
LABEL org.opencontainers.image.authors "Gaspard Wierzbinski (https://cpluspatch.dev)"
|
||||
LABEL org.opencontainers.image.source "https://github.com/lysand-org/lysand"
|
||||
LABEL org.opencontainers.image.vendor "Lysand Org"
|
||||
LABEL org.opencontainers.image.licenses "AGPL-3.0"
|
||||
LABEL org.opencontainers.image.title "Lysand Server"
|
||||
LABEL org.opencontainers.image.description "Lysand Server docker image"
|
||||
|
||||
# CD to app
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENTRYPOINT [ "/bin/sh", "/app/entrypoint.sh" ]
|
||||
# Run migrations and start the server
|
||||
CMD [ "start" ]
|
||||
3
build.ts
3
build.ts
|
|
@ -2,6 +2,9 @@
|
|||
import { $ } from "bun";
|
||||
import { rawRoutes } from "~routes";
|
||||
|
||||
const feOnly = process.argv.includes("--frontend");
|
||||
const serverOnly = process.argv.includes("--server");
|
||||
|
||||
console.log("Building frontend...");
|
||||
|
||||
await $`bun fe:build`;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
---
|
||||
services:
|
||||
lysand:
|
||||
image: ghcr.io/lysand-org/lysand:main
|
||||
build: . # Automatic builds are currently broken, please build from source
|
||||
# image: ghcr.io/lysand-org/lysand:latest
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./config:/app/config
|
||||
- ./uploads:/app/uploads
|
||||
- ./logs:/app/dist/logs
|
||||
- ./config:/app/dist/config
|
||||
- ./uploads:/app/dist/uploads
|
||||
restart: unless-stopped
|
||||
container_name: lysand
|
||||
networks:
|
||||
- lysand-net
|
||||
|
||||
db:
|
||||
image: ghcr.io/lysand-org/postgres:main
|
||||
container_name: lysand-db
|
||||
|
|
@ -17,32 +18,33 @@ services:
|
|||
environment:
|
||||
POSTGRES_DB: lysand
|
||||
POSTGRES_USER: lysand
|
||||
POSTGRES_PASSWORD: lysand
|
||||
POSTGRES_PASSWORD: _______________
|
||||
networks:
|
||||
- lysand-net
|
||||
volumes:
|
||||
- ./db-data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
image: redis:alpine
|
||||
container_name: lysand-redis
|
||||
volumes:
|
||||
- ./redis-data:/data
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- lysand-net
|
||||
|
||||
meilisearch:
|
||||
stdin_open: true
|
||||
environment:
|
||||
- MEILI_MASTER_KEY=add_your_key_here
|
||||
- MEILI_MASTER_KEY=__________________
|
||||
tty: true
|
||||
networks:
|
||||
- lysand-net
|
||||
volumes:
|
||||
- ./meili-data:/meili_data
|
||||
image: getmeili/meilisearch:v1.5
|
||||
image: getmeili/meilisearch:v1.7
|
||||
container_name: lysand-meilisearch
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
lysand-net:
|
||||
internal: true
|
||||
|
|
|
|||
|
|
@ -136,12 +136,8 @@ export class RequestParser {
|
|||
* @private
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
private async parseJson<T>(): Promise<Partial<T>> {
|
||||
try {
|
||||
return (await this.request.json()) as T;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
private async parseJson<T>(): Promise<T> {
|
||||
return (await this.request.json()) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
178
packages/server-handler/index.ts
Normal file
178
packages/server-handler/index.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { errorResponse, jsonResponse, response } from "@response";
|
||||
import type { MatchedRoute } from "bun";
|
||||
import { type Config, config } from "config-manager";
|
||||
import { LogLevel, type LogManager, type MultiLogManager } from "log-manager";
|
||||
import { RequestParser } from "request-parser";
|
||||
import { type ZodType, z } from "zod";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import {
|
||||
type AuthData,
|
||||
type UserWithRelations,
|
||||
getFromRequest,
|
||||
} from "~database/entities/User";
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
type HttpVerb = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
|
||||
|
||||
export type RouteHandler<
|
||||
RouteMeta extends APIRouteMetadata,
|
||||
ZodSchema extends ZodType,
|
||||
> = (
|
||||
req: Request,
|
||||
matchedRoute: MatchedRoute,
|
||||
extraData: {
|
||||
auth: {
|
||||
// If the route doesn't require authentication, set the type to UserWithRelations | null
|
||||
// Otherwise set to UserWithRelations
|
||||
user: RouteMeta["auth"]["required"] extends true
|
||||
? UserWithRelations
|
||||
: UserWithRelations | null;
|
||||
token: RouteMeta["auth"]["required"] extends true
|
||||
? string
|
||||
: string | null;
|
||||
};
|
||||
parsedRequest: z.infer<ZodSchema>;
|
||||
configManager: {
|
||||
getConfig: () => Promise<Config>;
|
||||
};
|
||||
},
|
||||
) => MaybePromise<Response> | MaybePromise<object>;
|
||||
|
||||
export interface APIRouteMetadata {
|
||||
allowedMethods: HttpVerb[];
|
||||
ratelimits: {
|
||||
max: number;
|
||||
duration: number;
|
||||
};
|
||||
route: string;
|
||||
auth: {
|
||||
required: boolean;
|
||||
requiredOnMethods?: HttpVerb[];
|
||||
oauthPermissions?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface APIRouteExports {
|
||||
meta: APIRouteMetadata;
|
||||
schema: z.AnyZodObject;
|
||||
default: RouteHandler<APIRouteMetadata, z.AnyZodObject>;
|
||||
}
|
||||
|
||||
const exampleZodSchema = z.object({
|
||||
allowedMethods: z.array(z.string()),
|
||||
ratelimits: z.object({
|
||||
max: z.number(),
|
||||
duration: z.number(),
|
||||
}),
|
||||
route: z.string(),
|
||||
auth: z.object({
|
||||
required: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const processRoute = async (
|
||||
matchedRoute: MatchedRoute,
|
||||
request: Request,
|
||||
logger: LogManager | MultiLogManager,
|
||||
): Promise<Response> => {
|
||||
if (request.method === "OPTIONS") {
|
||||
return response();
|
||||
}
|
||||
|
||||
const route: APIRouteExports | null = await import(
|
||||
matchedRoute.filePath
|
||||
).catch(() => null);
|
||||
|
||||
if (!route) {
|
||||
return errorResponse("Route not found", 404);
|
||||
}
|
||||
|
||||
// Check if the request method is allowed
|
||||
if (!route.meta.allowedMethods.includes(request.method as HttpVerb)) {
|
||||
return errorResponse("Method not allowed", 405);
|
||||
}
|
||||
|
||||
let auth: AuthData | null = null;
|
||||
|
||||
if (
|
||||
route.meta.auth.required ||
|
||||
route.meta.auth.requiredOnMethods?.includes(request.method as HttpVerb)
|
||||
) {
|
||||
auth = await getFromRequest(request);
|
||||
|
||||
if (!auth.user) {
|
||||
return errorResponse(
|
||||
"Unauthorized: access to this method requires an authenticated user",
|
||||
401,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if Content-Type header is missing in POST, PUT and PATCH requests
|
||||
if (["POST", "PUT", "PATCH"].includes(request.method)) {
|
||||
if (!request.headers.has("Content-Type")) {
|
||||
return errorResponse(
|
||||
`Content-Type header is missing but required on method ${request.method}`,
|
||||
400,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const parsedRequest = await new RequestParser(request)
|
||||
.toObject()
|
||||
.catch(async (err) => {
|
||||
await logger.logError(
|
||||
LogLevel.ERROR,
|
||||
"Server.RouteRequestParser",
|
||||
err as Error,
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!parsedRequest) {
|
||||
return errorResponse(
|
||||
"The request could not be parsed, it may be malformed",
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
const parsingResult = route.schema?.safeParse(parsedRequest);
|
||||
|
||||
if (parsingResult && !parsingResult.success) {
|
||||
// Return a 422 error with the first error message
|
||||
return errorResponse(fromZodError(parsingResult.error).toString(), 422);
|
||||
}
|
||||
|
||||
try {
|
||||
const output = await route.default(request, matchedRoute, {
|
||||
auth: {
|
||||
token: auth?.token ?? null,
|
||||
user: auth?.user ?? null,
|
||||
},
|
||||
parsedRequest: parsingResult
|
||||
? (parsingResult.data as z.infer<typeof route.schema>)
|
||||
: parsedRequest,
|
||||
configManager: {
|
||||
getConfig: async () => config as Config,
|
||||
},
|
||||
});
|
||||
|
||||
// If the output is a normal JS object and not a Response, convert it to a jsonResponse
|
||||
if (!(output instanceof Response)) {
|
||||
return jsonResponse(output);
|
||||
}
|
||||
|
||||
return output;
|
||||
} catch (err) {
|
||||
await logger.logError(
|
||||
LogLevel.ERROR,
|
||||
"Server.RouteHandler",
|
||||
err as Error,
|
||||
);
|
||||
|
||||
return errorResponse(
|
||||
`A server error occured: ${(err as Error).message}`,
|
||||
500,
|
||||
);
|
||||
}
|
||||
};
|
||||
6
packages/server-handler/package.json
Normal file
6
packages/server-handler/package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "server-handler",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": { "zod": "^3.22.4", "zod-validation-error": "^3.1.0" }
|
||||
}
|
||||
363
packages/server-handler/tests.test.ts
Normal file
363
packages/server-handler/tests.test.ts
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
import { afterAll, describe, expect, it, mock } from "bun:test";
|
||||
import type { MatchedRoute } from "bun";
|
||||
import { LogManager } from "log-manager";
|
||||
import { z } from "zod";
|
||||
import { getTestUsers } from "~tests/utils";
|
||||
import { type APIRouteExports, processRoute } from ".";
|
||||
|
||||
describe("Route Processor", () => {
|
||||
it("should return a Response", async () => {
|
||||
mock.module(
|
||||
"./route",
|
||||
() =>
|
||||
({
|
||||
default: async () => new Response(),
|
||||
meta: {
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/route",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
schema: z.object({}),
|
||||
}) as APIRouteExports,
|
||||
);
|
||||
|
||||
const output = await processRoute(
|
||||
{
|
||||
filePath: "./route",
|
||||
} as MatchedRoute,
|
||||
new Request("https://test.com/route", {
|
||||
method: "GET",
|
||||
}),
|
||||
new LogManager(Bun.file("/dev/null")),
|
||||
);
|
||||
|
||||
expect(output).toBeInstanceOf(Response);
|
||||
});
|
||||
|
||||
it("should return a 404 when the route does not exist", async () => {
|
||||
const output = await processRoute(
|
||||
{
|
||||
filePath: "./nonexistent-route",
|
||||
} as MatchedRoute,
|
||||
new Request("https://test.com/nonexistent-route"),
|
||||
new LogManager(Bun.file("/dev/null")),
|
||||
);
|
||||
|
||||
expect(output.status).toBe(404);
|
||||
});
|
||||
|
||||
it("should return a 405 when the request method is not allowed", async () => {
|
||||
mock.module(
|
||||
"./route",
|
||||
() =>
|
||||
({
|
||||
default: async () => new Response(),
|
||||
meta: {
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/route",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
schema: z.object({}),
|
||||
}) as APIRouteExports,
|
||||
);
|
||||
|
||||
const output = await processRoute(
|
||||
{
|
||||
filePath: "./route",
|
||||
} as MatchedRoute,
|
||||
new Request("https://test.com/route", {
|
||||
method: "GET",
|
||||
}),
|
||||
new LogManager(Bun.file("/dev/null")),
|
||||
);
|
||||
|
||||
expect(output.status).toBe(405);
|
||||
});
|
||||
|
||||
it("should return a 401 when the route requires authentication but no user is authenticated", async () => {
|
||||
mock.module(
|
||||
"./route",
|
||||
() =>
|
||||
({
|
||||
default: async () => new Response(),
|
||||
meta: {
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/route",
|
||||
auth: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
schema: z.object({}),
|
||||
}) as APIRouteExports,
|
||||
);
|
||||
|
||||
const output = await processRoute(
|
||||
{
|
||||
filePath: "./route",
|
||||
} as MatchedRoute,
|
||||
new Request("https://test.com/route", {
|
||||
method: "POST",
|
||||
}),
|
||||
new LogManager(Bun.file("/dev/null")),
|
||||
);
|
||||
|
||||
expect(output.status).toBe(401);
|
||||
});
|
||||
|
||||
it("should return a 400 when the Content-Type header is missing in POST, PUT and PATCH requests", async () => {
|
||||
mock.module(
|
||||
"./route",
|
||||
() =>
|
||||
({
|
||||
default: async () => new Response(),
|
||||
meta: {
|
||||
allowedMethods: ["POST", "PUT", "PATCH"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/route",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
schema: z.object({}),
|
||||
}) as APIRouteExports,
|
||||
);
|
||||
|
||||
const output = await processRoute(
|
||||
{
|
||||
filePath: "./route",
|
||||
} as MatchedRoute,
|
||||
new Request("https://test.com/route", {
|
||||
method: "POST",
|
||||
}),
|
||||
new LogManager(Bun.file("/dev/null")),
|
||||
);
|
||||
|
||||
expect(output.status).toBe(400);
|
||||
|
||||
const output2 = await processRoute(
|
||||
{
|
||||
filePath: "./route",
|
||||
} as MatchedRoute,
|
||||
new Request("https://test.com/route", {
|
||||
method: "PUT",
|
||||
}),
|
||||
new LogManager(Bun.file("/dev/null")),
|
||||
);
|
||||
|
||||
expect(output2.status).toBe(400);
|
||||
|
||||
const output3 = await processRoute(
|
||||
{
|
||||
filePath: "./route",
|
||||
} as MatchedRoute,
|
||||
new Request("https://test.com/route", {
|
||||
method: "PATCH",
|
||||
}),
|
||||
new LogManager(Bun.file("/dev/null")),
|
||||
);
|
||||
|
||||
expect(output3.status).toBe(400);
|
||||
});
|
||||
|
||||
it("should return a 400 when the request could not be parsed", async () => {
|
||||
mock.module(
|
||||
"./route",
|
||||
() =>
|
||||
({
|
||||
default: async () => new Response(),
|
||||
meta: {
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/route",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
schema: z.object({}),
|
||||
}) as APIRouteExports,
|
||||
);
|
||||
|
||||
const output = await processRoute(
|
||||
{
|
||||
filePath: "./route",
|
||||
} as MatchedRoute,
|
||||
new Request("https://test.com/route", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: "invalid-json",
|
||||
}),
|
||||
new LogManager(Bun.file("/dev/null")),
|
||||
);
|
||||
|
||||
expect(output.status).toBe(400);
|
||||
});
|
||||
|
||||
it("should return a 422 when the request does not match the schema", async () => {
|
||||
mock.module(
|
||||
"./route",
|
||||
() =>
|
||||
({
|
||||
default: async () => new Response(),
|
||||
meta: {
|
||||
allowedMethods: ["POST"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/route",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
schema: z.object({
|
||||
foo: z.string(),
|
||||
}),
|
||||
}) as APIRouteExports,
|
||||
);
|
||||
|
||||
const output = await processRoute(
|
||||
{
|
||||
filePath: "./route",
|
||||
} as MatchedRoute,
|
||||
new Request("https://test.com/route", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ bar: "baz" }),
|
||||
}),
|
||||
new LogManager(Bun.file("/dev/null")),
|
||||
);
|
||||
|
||||
expect(output.status).toBe(422);
|
||||
});
|
||||
|
||||
it("should convert any JS objects returned by the route to a Response", async () => {
|
||||
mock.module(
|
||||
"./route",
|
||||
() =>
|
||||
({
|
||||
default: async () => ({ status: 200 }),
|
||||
meta: {
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/route",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
schema: z.object({}),
|
||||
}) as APIRouteExports,
|
||||
);
|
||||
|
||||
const output = await processRoute(
|
||||
{
|
||||
filePath: "./route",
|
||||
} as MatchedRoute,
|
||||
new Request("https://test.com/route", {
|
||||
method: "GET",
|
||||
}),
|
||||
new LogManager(Bun.file("/dev/null")),
|
||||
);
|
||||
|
||||
expect(output.status).toBe(200);
|
||||
});
|
||||
|
||||
it("should handle route errors", async () => {
|
||||
mock.module(
|
||||
"./route",
|
||||
() =>
|
||||
({
|
||||
default: async () => {
|
||||
throw new Error("Route error");
|
||||
},
|
||||
meta: {
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/route",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
schema: z.object({}),
|
||||
}) as APIRouteExports,
|
||||
);
|
||||
|
||||
const output = await processRoute(
|
||||
{
|
||||
filePath: "./route",
|
||||
} as MatchedRoute,
|
||||
new Request("https://test.com/route", {
|
||||
method: "GET",
|
||||
}),
|
||||
new LogManager(Bun.file("/dev/null")),
|
||||
);
|
||||
|
||||
expect(output.status).toBe(500);
|
||||
});
|
||||
|
||||
it("should return the route output when everything is valid", async () => {
|
||||
mock.module(
|
||||
"./route",
|
||||
() =>
|
||||
({
|
||||
default: async () => new Response("OK"),
|
||||
meta: {
|
||||
allowedMethods: ["GET"],
|
||||
ratelimits: {
|
||||
max: 100,
|
||||
duration: 60,
|
||||
},
|
||||
route: "/route",
|
||||
auth: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
schema: z.object({}),
|
||||
}) as APIRouteExports,
|
||||
);
|
||||
|
||||
const output = await processRoute(
|
||||
{
|
||||
filePath: "./route",
|
||||
} as MatchedRoute,
|
||||
new Request("https://test.com/route", {
|
||||
method: "GET",
|
||||
}),
|
||||
new LogManager(Bun.file("/dev/null")),
|
||||
);
|
||||
|
||||
expect(output.status).toBe(200);
|
||||
expect(await output.text()).toBe("OK");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import type { APIRouteExports } from "~packages/server-handler";
|
||||
import type { RouteHandler } from "./server/api/routes.type";
|
||||
import type { APIRouteMeta } from "./types/api";
|
||||
|
||||
|
|
@ -107,10 +108,6 @@ export const matchRoute = async <T = Record<string, never>>(url: string) => {
|
|||
if (!route) return { file: null, matchedRoute: null };
|
||||
|
||||
return {
|
||||
file: (await import(rawRoutes[route.name])) as {
|
||||
default: RouteHandler<T>;
|
||||
meta: APIRouteMeta;
|
||||
},
|
||||
matchedRoute: route,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
94
server.ts
94
server.ts
|
|
@ -1,15 +1,9 @@
|
|||
import {
|
||||
clientResponse,
|
||||
errorResponse,
|
||||
jsonResponse,
|
||||
response,
|
||||
} from "@response";
|
||||
import { errorResponse, response } from "@response";
|
||||
import type { Config } from "config-manager";
|
||||
import { matches } from "ip-matching";
|
||||
import type { LogManager, MultiLogManager } from "log-manager";
|
||||
import { LogLevel } from "log-manager";
|
||||
import { RequestParser } from "request-parser";
|
||||
import { getFromRequest } from "~database/entities/User";
|
||||
import { processRoute } from "~packages/server-handler";
|
||||
import { matchRoute } from "~routes";
|
||||
|
||||
export const createServer = (
|
||||
|
|
@ -97,91 +91,13 @@ export const createServer = (
|
|||
);
|
||||
}
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return jsonResponse({});
|
||||
}
|
||||
|
||||
// If route is .well-known, remove dot because the filesystem router can't handle dots for some reason
|
||||
const { file: filePromise, matchedRoute } = await matchRoute(
|
||||
const { matchedRoute } = await matchRoute(
|
||||
req.url.replace(".well-known", "well-known"),
|
||||
);
|
||||
|
||||
const file = filePromise;
|
||||
|
||||
if (matchedRoute && file === undefined) {
|
||||
await logger.log(
|
||||
LogLevel.ERROR,
|
||||
"Server",
|
||||
`Route file ${matchedRoute.filePath} not found or not registered in the routes file`,
|
||||
);
|
||||
|
||||
return errorResponse("Route not found", 500);
|
||||
}
|
||||
|
||||
if (matchedRoute && matchedRoute.name !== "/[...404]" && file) {
|
||||
const meta = file.meta;
|
||||
|
||||
// Check for allowed requests
|
||||
// @ts-expect-error Stupid error
|
||||
if (!meta.allowedMethods.includes(req.method)) {
|
||||
return errorResponse(
|
||||
`Method not allowed: allowed methods are: ${meta.allowedMethods.join(
|
||||
", ",
|
||||
)}`,
|
||||
405,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Check for ratelimits
|
||||
const auth = await getFromRequest(req);
|
||||
|
||||
// Check for authentication if required
|
||||
if (
|
||||
(meta.auth.required ||
|
||||
(meta.auth.requiredOnMethods ?? []).includes(
|
||||
// @ts-expect-error Stupid error
|
||||
req.method,
|
||||
)) &&
|
||||
!auth.user
|
||||
) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
// Check is Content-Type header is missing in relevant requests
|
||||
if (["POST", "PUT", "PATCH"].includes(req.method)) {
|
||||
if (!req.headers.has("Content-Type")) {
|
||||
return errorResponse(
|
||||
`Content-Type header is missing but required on method ${req.method}`,
|
||||
400,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let parsedRequest = {};
|
||||
|
||||
try {
|
||||
parsedRequest = await new RequestParser(req).toObject();
|
||||
} catch (e) {
|
||||
await logger.logError(
|
||||
LogLevel.ERROR,
|
||||
"Server.RouteRequestParser",
|
||||
e as Error,
|
||||
);
|
||||
return errorResponse("Bad request", 400);
|
||||
}
|
||||
|
||||
return await file.default(req.clone(), matchedRoute, {
|
||||
auth,
|
||||
parsedRequest,
|
||||
// To avoid having to rewrite each route
|
||||
configManager: {
|
||||
getConfig: () => Promise.resolve(config),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (new URL(req.url).pathname.startsWith("/api")) {
|
||||
return errorResponse("Route not found", 404);
|
||||
if (matchedRoute && matchedRoute.name !== "/[...404]") {
|
||||
return await processRoute(matchedRoute, req, logger);
|
||||
}
|
||||
|
||||
const base_url_with_http = config.http.base_url.replace(
|
||||
|
|
|
|||
Loading…
Reference in a new issue