import { Router } from "express"; import * as searchService from "../services/search.service.js"; import * as entryService from "../services/entry.service.js"; import * as tagService from "../services/tag.service.js"; import * as commentService from "../services/comment.service.js"; import * as sourceService from "../services/source.service.js"; import * as userService from "../services/user.service.js"; import { ENTRY_TYPES } from "../services/entry.service.js"; export const pagesRouter = Router(); function csv(v) { return String(v || "") .split(",") .map((s) => s.trim()) .filter(Boolean); } pagesRouter.get("/healthz", (_req, res) => { res.json({ ok: true, uptime: process.uptime() }); }); pagesRouter.get("/", async (_req, res, next) => { try { const [stats, recent, hero] = await Promise.all([ searchService.stats(), entryService.listRecent(4), (async () => { const rand = await searchService.randomActive(); return rand ? entryService.getById(rand.id) : null; })(), ]); res.render("index", { title: "Awesome Index · 把网上牛逼的东西收进一个库", stats, recent, hero, }); } catch (e) { next(e); } }); pagesRouter.get("/entries", async (req, res, next) => { try { const filters = { q: req.query.q ?? "", types: csv(req.query.type), tagIds: csv(req.query.tags).map(Number).filter(Number.isFinite), sort: req.query.sort ?? "updated", page: Math.max(1, Number(req.query.page) || 1), status: "active", }; const [result, tagTree, typeCounts] = await Promise.all([ searchService.searchEntries({ ...filters, perPage: 10 }), tagService.attachCounts(await tagService.buildTree()), searchService.typeCounts(), ]); const totalPages = Math.max(1, Math.ceil(result.total / result.perPage)); res.render("list", { title: `搜索${filters.q ? `:${filters.q}` : "全部"} · Awesome Index`, result, filters, totalPages, types: ENTRY_TYPES, tagTree, typeCounts, }); } catch (e) { next(e); } }); pagesRouter.get("/entries/:slug", async (req, res, next) => { try { const entry = await entryService.getBySlug(req.params.slug); if (!entry || entry.status === "greyed") { return res.status(404).send("条目不存在或已下架"); } const comments = await commentService.listForEntry(entry.id); const machine = { id: entry.slug, type: entry.type, name: entry.title, summary: entry.summary, description: entry.description_md, url: entry.url, tags: entry.tags, license: entry.license, stars: Number(entry.stars), updated_at: entry.updated_at, status: entry.status, }; res.render("detail", { title: `${entry.title} · Awesome Index`, entry, comments, machineJson: JSON.stringify(machine, null, 2), }); } catch (e) { next(e); } }); pagesRouter.get("/random", async (_req, res, next) => { try { const entry = await searchService.randomActive(); if (!entry) return res.redirect("/entries"); res.redirect(`/entries/${entry.slug}`); } catch (e) { next(e); } }); pagesRouter.get("/login", (_req, res) => { res.render("login", { title: "管理台登录 · Awesome Index" }); }); pagesRouter.get("/admin", async (req, res, next) => { try { if (!req.session.userId) return res.redirect("/login"); const isAdmin = req.session.role === "admin"; const [sources, users, comments, entries, tagTreeAdmin] = await Promise.all([ sourceService.listSources(), isAdmin ? userService.listUsers() : Promise.resolve([]), commentService.listAll({}), searchService.searchEntries({ status: "all", perPage: 50 }), tagService.attachCounts(await tagService.buildTree()), ]); res.render("admin", { title: "管理台 · Awesome Index", user: { username: req.session.username, role: req.session.role }, sources, users, comments, entries: entries.items, isAdmin, tagTreeAdmin, }); } catch (e) { next(e); } }); pagesRouter.get("/admin/entries/:id/edit", async (req, res, next) => { try { if (!req.session.userId) return res.redirect("/login"); const entry = await entryService.getById(Number(req.params.id)); if (!entry) throw Object.assign(new Error("条目不存在"), { status: 404 }); const tagTreeAdmin = await tagService.buildTree(); res.render("admin-entry-edit", { title: `编辑 · ${entry.title} · Awesome Index`, user: { username: req.session.username, role: req.session.role }, isAdmin: req.session.role === "admin", entry, tagTreeAdmin, types: ENTRY_TYPES, }); } catch (e) { next(e); } }); pagesRouter.post("/admin/entries/:id/edit", async (req, res, next) => { try { if (!req.session.userId) return res.redirect("/login"); const id = Number(req.params.id); const tags = Array.isArray(req.body.tags) ? req.body.tags : req.body.tags ? [req.body.tags] : []; const newTags = String(req.body.new_tags || "") .split(",") .map((s) => s.trim()) .filter(Boolean); const patch = { title: String(req.body.title || "").trim(), type: req.body.type, summary: String(req.body.summary || "").trim(), description_md: String(req.body.description_md || ""), url: String(req.body.url || "").trim(), license: String(req.body.license || "").trim(), stars: Number(req.body.stars) || 0, tags: [...new Set([...tags, ...newTags])], }; await entryService.updateEntry(id, patch, { isHumanEditor: true }); const status = req.body.status; if (status && entryService.STATUSES.includes(status)) { await entryService.setStatusDirect(id, status); } res.redirect("/admin#sec-content"); } catch (e) { next(e); } });