← back to Designer Portfolio Pages
src/index.ts
209 lines
import express from "express";
import cookieParser from "cookie-parser";
import basicAuth from "express-basic-auth";
import path from "node:path";
import { fileURLToPath } from "node:url";
import rateLimit from "express-rate-limit";
import { loadEnv } from "./env.js";
import { openDb, getPortfolioBySlug, insertPortfolio } from "./db.js";
import { CreatePortfolioSchema } from "./schemas.js";
import {
createSlug,
hashPasswordIfProvided,
isValidPortfolioAuthCookieValue,
makePortfolioAuthCookieValue,
verifyPassword
} from "./auth.js";
import { getPatternsByIds, getPatternsByQuery } from "./stevesTool.js";
import { maybeGenerateAiIntro } from "./aiIntro.js";
const env = loadEnv();
const db = openDb(env.SQLITE_PATH);
const app = express();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));
app.use(express.json({ limit: "1mb" }));
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use("/public", express.static(path.join(__dirname, "..", "public")));
// Global basic auth — protects builder & API per CLAUDE.md security rules.
// Portfolio share links (/p/:slug) are exempted so clients can view without credentials.
const auth = basicAuth({
users: { admin: "DWSecure2024!" },
challenge: true,
realm: "Designer Mood Board"
});
// Apply auth to everything EXCEPT /p/ share links and static assets
app.use((req, res, next) => {
if (req.path.startsWith("/p/") || req.path.startsWith("/public/")) {
return next();
}
return auth(req, res, next);
});
const createPortfolioLimiter = rateLimit({
windowMs: 60_000,
limit: 30
});
const passwordLimiter = rateLimit({
windowMs: 60_000,
limit: 20
});
// --- Routes ---
app.get("/", (_req, res) => res.redirect("/builder"));
app.get("/builder", (_req, res) => {
res.render("builder", { baseUrl: env.BASE_URL });
});
app.get("/api/patterns/search", async (req, res) => {
try {
const q = String(req.query.q ?? "");
const patterns = await getPatternsByQuery(q);
res.json({ patterns: patterns.slice(0, 40) });
} catch (err: any) {
res.status(500).json({ error: err?.message ?? "Unknown error" });
}
});
app.post("/api/portfolios", createPortfolioLimiter, async (req, res) => {
const parsed = CreatePortfolioSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: "Invalid request", details: parsed.error.issues });
}
try {
const input = parsed.data;
const patterns = await getPatternsByIds(input.patternIds);
if (!patterns.length) {
return res.status(400).json({ error: "No patterns found for provided IDs." });
}
const nowIso = new Date().toISOString();
const slug = createSlug(9);
const passwordHash = await hashPasswordIfProvided(input.password);
const aiIntro =
input.generateAiIntro
? await maybeGenerateAiIntro({
anthropicApiKey: env.ANTHROPIC_API_KEY,
anthropicModel: env.ANTHROPIC_MODEL,
designerName: input.contact.name,
businessName: input.contact.business || undefined,
title: input.title || undefined,
patterns
})
: null;
insertPortfolio(db, {
slug,
title: input.title || undefined,
createdAtIso: nowIso,
updatedAtIso: nowIso,
contact: {
name: input.contact.name,
business: input.contact.business || undefined,
email: input.contact.email,
phone: input.contact.phone || undefined,
website: input.contact.website || undefined
},
passwordHash,
patterns,
aiIntro
});
const shareUrl = `${env.BASE_URL}/p/${slug}`;
res.json({ slug, shareUrl });
} catch (err: any) {
res.status(500).json({ error: err?.message ?? "Unknown error" });
}
});
app.get("/p/:slug", (req, res) => {
const slug = String(req.params.slug);
const portfolio = getPortfolioBySlug(db, slug);
if (!portfolio) {
return res.status(404).send("Not found");
}
const needsPassword = Boolean(portfolio.passwordHash);
const authCookieName = `pp_auth_${slug}`;
const cookieValue = req.cookies?.[authCookieName] as string | undefined;
const isAuthed = !needsPassword || isValidPortfolioAuthCookieValue(env.COOKIE_SECRET, slug, cookieValue);
if (needsPassword && !isAuthed) {
return res.render("password", {
slug,
title: portfolio.title || "Portfolio",
business: portfolio.contact.business || "",
error: ""
});
}
return res.render("portfolio", {
portfolio,
baseUrl: env.BASE_URL
});
});
app.post("/p/:slug/auth", passwordLimiter, async (req, res) => {
const slug = String(req.params.slug);
const portfolio = getPortfolioBySlug(db, slug);
if (!portfolio) {
return res.status(404).send("Not found");
}
if (!portfolio.passwordHash) {
return res.redirect(`/p/${slug}`);
}
const password = String(req.body.password ?? "");
const ok = await verifyPassword(password, portfolio.passwordHash);
if (!ok) {
return res.status(401).render("password", {
slug,
title: portfolio.title || "Portfolio",
business: portfolio.contact.business || "",
error: "Incorrect password."
});
}
const authCookieName = `pp_auth_${slug}`;
const value = makePortfolioAuthCookieValue(env.COOKIE_SECRET, slug);
res.cookie(authCookieName, value, {
httpOnly: true,
sameSite: "lax",
secure: false,
maxAge: 1000 * 60 * 60 * 24 * 30
});
return res.redirect(`/p/${slug}`);
});
app.get("/share/:slug", (req, res) => {
const slug = String(req.params.slug);
res.render("share", { shareUrl: `${env.BASE_URL}/p/${slug}` });
});
app.listen(Number(env.PORT), () => {
console.log(`Designer portfolio pages running on ${env.BASE_URL}`);
});