This commit is contained in:
Debian
2026-03-30 14:03:44 +00:00
parent bb8497d33d
commit 8fe8d74644
7 changed files with 153 additions and 2 deletions
+32 -2
View File
@@ -7,6 +7,37 @@ type DockerResult = {
stderr: string;
};
const toDockerError = (stderr: string, stdout: string): HttpError => {
const details = (stderr || stdout).trim();
const normalized = details.toLowerCase();
if (
normalized.includes("permission denied while trying to connect to the docker api") ||
normalized.includes("permission denied") && normalized.includes("docker.sock")
) {
return new HttpError(
502,
"The API process cannot access Docker. Grant access to /var/run/docker.sock, run the API with a user in the docker group, or mount the Docker socket into the API container."
);
}
if (
normalized.includes("cannot connect to the docker daemon") ||
normalized.includes("is the docker daemon running")
) {
return new HttpError(502, "Docker is not reachable. Check that the Docker daemon is running and accessible to the API process.");
}
if (
normalized.includes("no such container") ||
normalized.includes("no such object")
) {
return new HttpError(502, `Docker Mailserver container '${config.mailserverContainer}' was not found.`);
}
return new HttpError(502, details || "Docker Mailserver command failed.");
};
export const runDocker = async (
args: string[],
input?: string
@@ -48,7 +79,7 @@ export const runDocker = async (
});
if (exitCode !== 0) {
throw new HttpError(502, stderr.trim() || stdout.trim() || "Docker Mailserver command failed.");
throw toDockerError(stderr, stdout);
}
return {
@@ -69,4 +100,3 @@ export const runDocker = async (
clearTimeout(timeout);
}
};
+11
View File
@@ -3,6 +3,7 @@ import { loginAdmin, logoutAdmin, requireAdminSession, getSessionFromRequest, se
import { cleanupDatabase } from "./lib/db";
import { errorResponse, jsonResponse, emptyResponse, optionsResponse, parseJson, assertAllowedOrigin } from "./lib/http";
import { config } from "./lib/config";
import { serveFrontendBuild } from "./lib/frontend";
import {
createUserSchema,
loginSchema,
@@ -156,6 +157,16 @@ app.post(
})
);
app.get(
"/",
route(async ({ request }) => serveFrontendBuild(request))
);
app.get(
"/*",
route(async ({ request }) => serveFrontendBuild(request))
);
app.listen({
hostname: config.host,
port: config.port
+1
View File
@@ -32,5 +32,6 @@ export const config = {
appAdminEmail: env.APP_ADMIN_EMAIL?.trim().toLowerCase() ?? "",
appAdminPassword: env.APP_ADMIN_PASSWORD ?? "",
appMailFrom: env.APP_MAIL_FROM?.trim().toLowerCase() ?? "[email protected]",
webDistPath: resolve(process.cwd(), env.WEB_DIST_PATH ?? "../web/dist"),
isProduction: (env.NODE_ENV ?? "development") === "production"
};
+64
View File
@@ -0,0 +1,64 @@
import { resolve, sep } from "node:path";
import { config } from "./config";
const frontendRoot = resolve(config.webDistPath);
const indexFile = Bun.file(resolve(frontendRoot, "index.html"));
const noCacheHeaders = {
"Cache-Control": "no-cache"
};
const assetCacheHeaders = {
"Cache-Control": "public, max-age=31536000, immutable"
};
const toSafePath = (pathname: string): string | null => {
const cleaned = pathname.replace(/^\/+/, "");
const absolutePath = resolve(frontendRoot, cleaned);
if (absolutePath !== frontendRoot && !absolutePath.startsWith(`${frontendRoot}${sep}`)) {
return null;
}
return absolutePath;
};
const fileResponse = (file: Bun.BunFile, headers?: HeadersInit): Response =>
new Response(file, {
status: 200,
headers
});
export const serveFrontendBuild = async (request: Request): Promise<Response> => {
const { pathname } = new URL(request.url);
if (pathname === "/api" || pathname.startsWith("/api/")) {
return new Response("Not found", { status: 404 });
}
if (!(await indexFile.exists())) {
return new Response("Frontend build not found. Run `bun run build` first.", {
status: 503,
headers: {
"content-type": "text/plain; charset=utf-8"
}
});
}
if (pathname !== "/") {
const safePath = toSafePath(pathname);
if (!safePath) {
return new Response("Not found", { status: 404 });
}
const assetFile = Bun.file(safePath);
if (await assetFile.exists()) {
return fileResponse(
assetFile,
pathname.startsWith("/assets/") ? assetCacheHeaders : noCacheHeaders
);
}
}
return fileResponse(indexFile, noCacheHeaders);
};