119 lines
2.9 KiB
TypeScript
119 lines
2.9 KiB
TypeScript
import { ZodError, type ZodSchema } from "zod";
|
|
import { config } from "./config";
|
|
import { HttpError } from "./errors";
|
|
|
|
const allowedOrigin = (request: Request): string | null => {
|
|
const origin = request.headers.get("origin");
|
|
if (!origin) {
|
|
return null;
|
|
}
|
|
|
|
return config.appOrigins.includes(origin.toLowerCase()) ? origin : null;
|
|
};
|
|
|
|
const securityHeaders = (): Record<string, string> => ({
|
|
"X-Content-Type-Options": "nosniff",
|
|
"X-Frame-Options": "DENY",
|
|
"Referrer-Policy": "no-referrer"
|
|
});
|
|
|
|
const mergeHeaders = (request: Request, headers?: HeadersInit): Headers => {
|
|
const result = new Headers(headers);
|
|
const origin = allowedOrigin(request);
|
|
|
|
for (const [key, value] of Object.entries(securityHeaders())) {
|
|
result.set(key, value);
|
|
}
|
|
|
|
result.set("Vary", "Origin");
|
|
|
|
if (origin) {
|
|
result.set("Access-Control-Allow-Origin", origin);
|
|
result.set("Access-Control-Allow-Credentials", "true");
|
|
result.set("Access-Control-Allow-Headers", "Content-Type");
|
|
result.set("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE,OPTIONS");
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
export const assertAllowedOrigin = (request: Request): void => {
|
|
const origin = request.headers.get("origin");
|
|
if (!origin) {
|
|
return;
|
|
}
|
|
|
|
console.log(config.appOrigins, origin)
|
|
if (!config.appOrigins.includes(origin.toLowerCase())) {
|
|
throw new HttpError(403, "Origin is not allowed.");
|
|
}
|
|
};
|
|
|
|
export const jsonResponse = (
|
|
request: Request,
|
|
status: number,
|
|
payload: unknown,
|
|
headers?: HeadersInit
|
|
): Response =>
|
|
new Response(JSON.stringify(payload), {
|
|
status,
|
|
headers: mergeHeaders(request, {
|
|
"content-type": "application/json; charset=utf-8",
|
|
...headers
|
|
})
|
|
});
|
|
|
|
export const emptyResponse = (request: Request, status = 204, headers?: HeadersInit): Response =>
|
|
new Response(null, {
|
|
status,
|
|
headers: mergeHeaders(request, headers)
|
|
});
|
|
|
|
export const optionsResponse = (request: Request): Response =>
|
|
emptyResponse(request, 204);
|
|
|
|
export const parseJson = async <T>(request: Request, schema: ZodSchema<T>): Promise<T> => {
|
|
let body: unknown;
|
|
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
throw new HttpError(400, "Request body must be valid JSON.");
|
|
}
|
|
|
|
try {
|
|
return schema.parse(body);
|
|
} catch (error) {
|
|
if (error instanceof ZodError) {
|
|
throw new HttpError(400, error.issues[0]?.message ?? "Invalid request body.");
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const errorResponse = (request: Request, error: unknown): Response => {
|
|
if (error instanceof HttpError) {
|
|
return jsonResponse(request, error.status, {
|
|
error: {
|
|
message: error.message
|
|
}
|
|
}, error.headers);
|
|
}
|
|
|
|
if (error instanceof ZodError) {
|
|
return jsonResponse(request, 400, {
|
|
error: {
|
|
message: error.issues[0]?.message ?? "Invalid request."
|
|
}
|
|
});
|
|
}
|
|
|
|
console.error(error);
|
|
|
|
return jsonResponse(request, 500, {
|
|
error: {
|
|
message: "Unexpected server error."
|
|
}
|
|
});
|
|
};
|