* initial commit * format links right jobops.dakheera47.com/cv/shaheer-google-de * don't support legacy * remove phishing look * smaller links * readiness check in settings * rework UX * right col * pop a modal * modal improvements * show links * documentation disclaimer * fix(tracer-links): preserve descriptive resume link labels * fix(tracer-links): classify bot user agents before browser families * fix(tracer-links): reject non-http redirect destinations * fix(tracer-redirect): disable caching for tracked redirects * fix(origin): prefer canonical public base url over forwarded headers * fix(auth): protect tracer analytics routes behind basic auth * fix(ui): rename misleading tracer drilldown human metric * style(tests): format tracer-links invalid-destination assertion * fix(tests): prevent mocked fs from breaking sqlite data-dir resolution * style(docs): format versioned docs json for biome * fix(tests): mock tracer-links in pdf skills validation suite
247 lines
6.9 KiB
TypeScript
247 lines
6.9 KiB
TypeScript
/**
|
|
* Express app factory (useful for tests).
|
|
*/
|
|
|
|
import { existsSync } from "node:fs";
|
|
import { readFile } from "node:fs/promises";
|
|
import { dirname, extname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { unauthorized } from "@infra/errors";
|
|
import {
|
|
apiErrorHandler,
|
|
fail,
|
|
legacyApiResponseShim,
|
|
notFoundApiHandler,
|
|
requestContextMiddleware,
|
|
} from "@infra/http";
|
|
import { logger } from "@infra/logger";
|
|
import cors from "cors";
|
|
import express from "express";
|
|
import { apiRouter } from "./api/index";
|
|
import { getDataDir } from "./config/dataDir";
|
|
import { isDemoMode } from "./config/demo";
|
|
import { resolveTracerRedirect } from "./services/tracer-links";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
function createBasicAuthGuard() {
|
|
function getAuthConfig() {
|
|
const user = process.env.BASIC_AUTH_USER || "";
|
|
const pass = process.env.BASIC_AUTH_PASSWORD || "";
|
|
return {
|
|
user,
|
|
pass,
|
|
enabled: user.length > 0 && pass.length > 0,
|
|
};
|
|
}
|
|
|
|
function isAuthorized(req: express.Request): boolean {
|
|
const { user: authUser, pass: authPass, enabled } = getAuthConfig();
|
|
if (!enabled) return false;
|
|
const authHeader = req.headers.authorization || "";
|
|
if (!authHeader.startsWith("Basic ")) return false;
|
|
const encoded = authHeader.slice("Basic ".length).trim();
|
|
let decoded = "";
|
|
try {
|
|
decoded = Buffer.from(encoded, "base64").toString("utf-8");
|
|
} catch {
|
|
return false;
|
|
}
|
|
const separatorIndex = decoded.indexOf(":");
|
|
if (separatorIndex === -1) return false;
|
|
const user = decoded.slice(0, separatorIndex);
|
|
const pass = decoded.slice(separatorIndex + 1);
|
|
return user === authUser && pass === authPass;
|
|
}
|
|
|
|
function isPublicReadOnlyRoute(method: string, path: string): boolean {
|
|
const normalizedMethod = method.toUpperCase();
|
|
const normalizedPath = path.split("?")[0] || path;
|
|
if (
|
|
normalizedMethod === "POST" &&
|
|
normalizedPath === "/api/visa-sponsors/search"
|
|
)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
function requiresAuth(method: string, path: string): boolean {
|
|
if (isPublicReadOnlyRoute(method, path)) return false;
|
|
if (path.startsWith("/api/tracer-links")) {
|
|
return method.toUpperCase() !== "OPTIONS";
|
|
}
|
|
return !["GET", "HEAD", "OPTIONS"].includes(method.toUpperCase());
|
|
}
|
|
|
|
const middleware = (
|
|
req: express.Request,
|
|
res: express.Response,
|
|
next: express.NextFunction,
|
|
) => {
|
|
const { enabled } = getAuthConfig();
|
|
if (!enabled || !requiresAuth(req.method, req.path)) return next();
|
|
if (isAuthorized(req)) return next();
|
|
fail(res, unauthorized("Authentication required"));
|
|
};
|
|
|
|
return {
|
|
middleware,
|
|
isAuthorized,
|
|
basicAuthEnabled: getAuthConfig().enabled,
|
|
};
|
|
}
|
|
|
|
export function createApp() {
|
|
const app = express();
|
|
const authGuard = createBasicAuthGuard();
|
|
|
|
const handleTracerRedirect = async (
|
|
req: express.Request,
|
|
res: express.Response,
|
|
slug: string,
|
|
route: string,
|
|
) => {
|
|
try {
|
|
const redirect = await resolveTracerRedirect({
|
|
token: slug,
|
|
requestId:
|
|
(res.getHeader("x-request-id") as string | undefined) ?? null,
|
|
ip: req.ip ?? null,
|
|
userAgent: req.header("user-agent") ?? null,
|
|
referrer: req.header("referer") ?? null,
|
|
});
|
|
|
|
if (!redirect) {
|
|
logger.warn("Tracer link not found", {
|
|
route,
|
|
token: slug,
|
|
});
|
|
res.status(404).type("text/plain; charset=utf-8").send("Not found");
|
|
return;
|
|
}
|
|
|
|
logger.info("Tracer link redirected", {
|
|
route,
|
|
token: slug,
|
|
jobId: redirect.jobId,
|
|
});
|
|
res.set("Cache-Control", "no-store");
|
|
res.set("Pragma", "no-cache");
|
|
res.set("Expires", "0");
|
|
res.redirect(302, redirect.destinationUrl);
|
|
} catch (error) {
|
|
logger.error("Tracer redirect failed", {
|
|
route,
|
|
token: slug,
|
|
error,
|
|
});
|
|
res.status(500).type("text/plain; charset=utf-8").send("Internal error");
|
|
}
|
|
};
|
|
|
|
app.use(cors());
|
|
app.use(requestContextMiddleware());
|
|
app.use(express.json({ limit: "5mb" }));
|
|
app.use(legacyApiResponseShim());
|
|
|
|
// Logging middleware
|
|
app.use((req, res, next) => {
|
|
const start = Date.now();
|
|
res.on("finish", () => {
|
|
const duration = Date.now() - start;
|
|
logger.info("HTTP request completed", {
|
|
method: req.method,
|
|
path: req.path,
|
|
status: res.statusCode,
|
|
durationMs: duration,
|
|
});
|
|
});
|
|
next();
|
|
});
|
|
|
|
// Optional Basic Auth for write access (read-only by default)
|
|
app.use(authGuard.middleware);
|
|
|
|
// API routes
|
|
app.use("/api", apiRouter);
|
|
app.use(notFoundApiHandler());
|
|
|
|
app.get("/cv/:slug", async (req, res) => {
|
|
const slug = req.params.slug?.trim();
|
|
if (!slug) {
|
|
res.status(404).type("text/plain; charset=utf-8").send("Not found");
|
|
return;
|
|
}
|
|
await handleTracerRedirect(req, res, slug, "GET /cv/:slug");
|
|
});
|
|
|
|
// Serve static files for generated PDFs
|
|
const pdfDir = join(getDataDir(), "pdfs");
|
|
if (isDemoMode()) {
|
|
const demoPdfPath = join(pdfDir, "demo.pdf");
|
|
app.get("/pdfs/*", (_req, res) => {
|
|
res.sendFile(demoPdfPath, (error) => {
|
|
if (error) res.status(404).end();
|
|
});
|
|
});
|
|
}
|
|
app.use("/pdfs", express.static(pdfDir));
|
|
|
|
// Health check
|
|
app.get("/health", (_req, res) => {
|
|
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
// Serve client app in production
|
|
if (process.env.NODE_ENV === "production") {
|
|
const packagedDocsDir = join(__dirname, "../../dist/docs");
|
|
const workspaceDocsDir = join(__dirname, "../../../docs-site/build");
|
|
const docsDir = existsSync(packagedDocsDir)
|
|
? packagedDocsDir
|
|
: workspaceDocsDir;
|
|
const docsIndexPath = join(docsDir, "index.html");
|
|
let cachedDocsIndexHtml: string | null = null;
|
|
|
|
if (existsSync(docsIndexPath)) {
|
|
app.use("/docs", express.static(docsDir));
|
|
app.get("/docs/*", async (req, res, next) => {
|
|
if (!req.accepts("html")) {
|
|
next();
|
|
return;
|
|
}
|
|
if (extname(req.path)) {
|
|
next();
|
|
return;
|
|
}
|
|
if (!cachedDocsIndexHtml) {
|
|
cachedDocsIndexHtml = await readFile(docsIndexPath, "utf-8");
|
|
}
|
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
res.send(cachedDocsIndexHtml);
|
|
});
|
|
}
|
|
|
|
const clientDir = join(__dirname, "../../dist/client");
|
|
app.use(express.static(clientDir));
|
|
|
|
// SPA fallback
|
|
const indexPath = join(clientDir, "index.html");
|
|
let cachedIndexHtml: string | null = null;
|
|
app.get("*", async (req, res) => {
|
|
if (!req.accepts("html")) {
|
|
res.status(404).end();
|
|
return;
|
|
}
|
|
if (!cachedIndexHtml) {
|
|
cachedIndexHtml = await readFile(indexPath, "utf-8");
|
|
}
|
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
res.send(cachedIndexHtml);
|
|
});
|
|
}
|
|
|
|
app.use(apiErrorHandler);
|
|
|
|
return app;
|
|
}
|