Support flexible base paths and thin proxies
This commit is contained in:
+7
-3
@@ -2,7 +2,6 @@ import path from "node:path";
|
||||
|
||||
export interface AppConfig {
|
||||
basePath: string;
|
||||
baseUrl: string;
|
||||
port: number;
|
||||
dataDir: string;
|
||||
htmlDir: string;
|
||||
@@ -21,11 +20,16 @@ function intEnv(name: string, fallback: number): number {
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
function basePathEnv(): string {
|
||||
const raw = (process.env.AISHARE_BASE_PATH || "/aishare").trim();
|
||||
if (!raw || raw === "/") return "";
|
||||
return `/${raw.replace(/^\/+|\/+$/g, "")}`;
|
||||
}
|
||||
|
||||
const dataDir = process.env.AISHARE_DATA_DIR || path.resolve("data");
|
||||
|
||||
export const config: AppConfig = {
|
||||
basePath: "/aishare",
|
||||
baseUrl: (process.env.AISHARE_BASE_URL || "http://127.0.0.1:8088/aishare").replace(/\/$/, ""),
|
||||
basePath: basePathEnv(),
|
||||
port: intEnv("AISHARE_PORT", 8080),
|
||||
dataDir,
|
||||
htmlDir: path.join(dataDir, "html"),
|
||||
|
||||
+38
-22
@@ -24,6 +24,16 @@ app.disable("x-powered-by");
|
||||
app.enable("strict routing");
|
||||
app.use(express.json({ limit: "1mb" }));
|
||||
|
||||
function route(pathname = ""): string {
|
||||
if (!pathname) return config.basePath || "/";
|
||||
const suffix = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
||||
return `${config.basePath}${suffix}` || "/";
|
||||
}
|
||||
|
||||
function publicPath(slug: string): string {
|
||||
return `${config.basePath}/${slug}/` || `/${slug}/`;
|
||||
}
|
||||
|
||||
function requireAdmin(req: Request, res: Response, next: NextFunction): void {
|
||||
if (!config.adminToken) {
|
||||
res.status(500).json({ error: "AISHARE_ADMIN_TOKEN is not configured." });
|
||||
@@ -51,7 +61,7 @@ function publicCapture(row: ReturnType<typeof store.getCapture>) {
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
capturedAt: row.captured_at,
|
||||
publicUrl: row.slug ? `${config.baseUrl}/${row.slug}/` : null
|
||||
publicUrl: row.slug ? publicPath(row.slug) : null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,18 +76,18 @@ function safeSendFrom(root: string, requested: string, res: Response): void {
|
||||
res.sendFile(target);
|
||||
}
|
||||
|
||||
app.use(`${config.basePath}/assets`, express.static(path.resolve("dist-client/assets"), { immutable: true, maxAge: "1y" }));
|
||||
app.use(`${config.basePath}/admin-assets`, express.static(path.resolve("dist-client"), { immutable: true, maxAge: "1y" }));
|
||||
app.use(route("/assets"), express.static(path.resolve("dist-client/assets"), { immutable: true, maxAge: "1y" }));
|
||||
app.use(route("/admin-assets"), express.static(path.resolve("dist-client"), { immutable: true, maxAge: "1y" }));
|
||||
|
||||
app.get(`${config.basePath}/admin`, (_req, res) => {
|
||||
app.get(route("/admin"), (_req, res) => {
|
||||
res.sendFile(path.resolve("dist-client/index.html"));
|
||||
});
|
||||
|
||||
app.get(`${config.basePath}/api/health`, (_req, res) => {
|
||||
app.get(route("/api/health"), (_req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.post(`${config.basePath}/api/captures`, requireAdmin, (req, res) => {
|
||||
app.post(route("/api/captures"), requireAdmin, (req, res) => {
|
||||
try {
|
||||
const sourceUrl = validateChatGptShareUrl(String(req.body?.sourceUrl || ""));
|
||||
const requestedSlug = req.body?.slug ? slugify(String(req.body.slug)) : null;
|
||||
@@ -89,7 +99,7 @@ app.post(`${config.basePath}/api/captures`, requireAdmin, (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get(`${config.basePath}/api/captures`, requireAdmin, (req, res) => {
|
||||
app.get(route("/api/captures"), requireAdmin, (req, res) => {
|
||||
const page = Number.parseInt(String(req.query.page || "1"), 10);
|
||||
const pageSize = Number.parseInt(String(req.query.pageSize || "20"), 10);
|
||||
const result = store.listCaptures(page, pageSize);
|
||||
@@ -101,7 +111,7 @@ app.get(`${config.basePath}/api/captures`, requireAdmin, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.get(`${config.basePath}/api/captures/:id`, requireAdmin, (req, res) => {
|
||||
app.get(route("/api/captures/:id"), requireAdmin, (req, res) => {
|
||||
const row = store.getCapture(req.params.id);
|
||||
if (!row) {
|
||||
res.status(404).json({ error: "Capture not found." });
|
||||
@@ -110,7 +120,7 @@ app.get(`${config.basePath}/api/captures/:id`, requireAdmin, (req, res) => {
|
||||
res.json({ capture: publicCapture(row), errorDetail: row.error_detail, metadata: row.metadata_json ? JSON.parse(row.metadata_json) : null });
|
||||
});
|
||||
|
||||
app.post(`${config.basePath}/api/captures/:id/retry`, requireAdmin, (req, res) => {
|
||||
app.post(route("/api/captures/:id/retry"), requireAdmin, (req, res) => {
|
||||
const row = store.getCapture(req.params.id);
|
||||
if (!row) {
|
||||
res.status(404).json({ error: "Capture not found." });
|
||||
@@ -125,7 +135,7 @@ app.post(`${config.basePath}/api/captures/:id/retry`, requireAdmin, (req, res) =
|
||||
res.json({ capture: publicCapture(store.getCapture(row.id)) });
|
||||
});
|
||||
|
||||
app.delete(`${config.basePath}/api/captures/:id`, requireAdmin, async (req, res) => {
|
||||
app.delete(route("/api/captures/:id"), requireAdmin, async (req, res) => {
|
||||
const row = store.deleteCapture(req.params.id);
|
||||
if (!row) {
|
||||
res.status(404).json({ error: "Capture not found." });
|
||||
@@ -135,28 +145,28 @@ app.delete(`${config.basePath}/api/captures/:id`, requireAdmin, async (req, res)
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.delete(`${config.basePath}/api/captures`, requireAdmin, async (_req, res) => {
|
||||
app.delete(route("/api/captures"), requireAdmin, async (_req, res) => {
|
||||
store.deleteAllCaptures();
|
||||
await deleteAllArtifactRoots();
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.get(`${config.basePath}/:slug/assets/*`, (req, res) => {
|
||||
app.get(route("/:slug/assets/*"), (req, res) => {
|
||||
const wildcard = (req.params as Record<string, string>)[0] || "";
|
||||
safeSendFrom(artifactAssetsDir(req.params.slug), wildcard, res);
|
||||
});
|
||||
|
||||
app.get(`${config.basePath}/:slug/screenshots/:variant.png`, (req, res) => {
|
||||
app.get(route("/:slug/screenshots/:variant.png"), (req, res) => {
|
||||
res.sendFile(screenshotPath(req.params.slug, req.params.variant));
|
||||
});
|
||||
|
||||
app.get(`${config.basePath}/:slug/screenshot.png`, (req, res) => {
|
||||
app.get(route("/:slug/screenshot.png"), (req, res) => {
|
||||
const device = req.query.device === "mobile" ? "mobile" : "desktop";
|
||||
const theme = req.query.theme === "dark" ? "dark" : "light";
|
||||
res.sendFile(screenshotPath(req.params.slug, `${device}-${theme}`));
|
||||
});
|
||||
|
||||
app.get(`${config.basePath}/:slug/download.html`, (req, res) => {
|
||||
app.get(route("/:slug/download.html"), (req, res) => {
|
||||
const row = store.getCaptureBySlug(req.params.slug);
|
||||
if (!row) {
|
||||
res.status(404).send("Not found");
|
||||
@@ -165,7 +175,7 @@ app.get(`${config.basePath}/:slug/download.html`, (req, res) => {
|
||||
res.download(artifactIndexPath(req.params.slug), `${req.params.slug}.html`);
|
||||
});
|
||||
|
||||
app.get(`${config.basePath}/:slug/download`, async (req, res, next) => {
|
||||
app.get(route("/:slug/download"), async (req, res, next) => {
|
||||
try {
|
||||
const row = store.getCaptureBySlug(req.params.slug);
|
||||
if (!row) {
|
||||
@@ -187,7 +197,7 @@ app.get(`${config.basePath}/:slug/download`, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get(`${config.basePath}/:slug/download.zip`, async (req, res, next) => {
|
||||
app.get(route("/:slug/download.zip"), async (req, res, next) => {
|
||||
try {
|
||||
const row = store.getCaptureBySlug(req.params.slug);
|
||||
if (!row) {
|
||||
@@ -202,7 +212,7 @@ app.get(`${config.basePath}/:slug/download.zip`, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get(`${config.basePath}/:slug/`, async (req, res) => {
|
||||
app.get(route("/:slug/"), async (req, res) => {
|
||||
const row = store.getCaptureBySlug(req.params.slug);
|
||||
if (!row) {
|
||||
res.status(404).send("Not found");
|
||||
@@ -218,14 +228,20 @@ app.get(`${config.basePath}/:slug/`, async (req, res) => {
|
||||
.send(`<!doctype html><meta name="robots" content="noindex"><title>${row.title || "AI Share"}</title><p>${row.error_public || `Capture is ${row.status}.`}</p>`);
|
||||
});
|
||||
|
||||
app.get(`${config.basePath}/:slug`, (req, res) => {
|
||||
res.redirect(308, `${config.basePath}/${req.params.slug}/`);
|
||||
app.get(route("/:slug"), (req, res) => {
|
||||
res.redirect(308, `${req.originalUrl.replace(/[?#].*$/, "")}/`);
|
||||
});
|
||||
|
||||
app.get(config.basePath, (_req, res) => {
|
||||
res.redirect(301, `${config.basePath}/admin`);
|
||||
app.get(route(), (_req, res) => {
|
||||
res.redirect(301, route("/admin"));
|
||||
});
|
||||
|
||||
if (config.basePath) {
|
||||
app.get(route("/"), (_req, res) => {
|
||||
res.redirect(301, route("/admin"));
|
||||
});
|
||||
}
|
||||
|
||||
app.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: "Internal server error." });
|
||||
|
||||
Reference in New Issue
Block a user