NobGit
public anord read

Ledger

Why work hard when you can work easier?

Languages

Repository composition by tracked source files.

TypeScript
TypeScript 86% CSS 10% SQL 3% Shell 1% HTML 0%
Create file Wiki Documentation
Clone
https://nobgit.com/orgs/anord/ledger.git
ssh://[email protected]:2222/orgs/anord/ledger.git
pages.ts
import { Router } from "express";
import { z } from "zod";
import { requireEditor } from "../middleware/auth.js";
import {
  createOrUpdatePage,
  getPageBySlug,
  listPageRevisions,
  listDraftPages,
  listPagesForSpace,
  rollbackPageRevision
} from "../../services/pages.js";
import { enqueueWebhookEvent } from "../../services/webhooks.js";
import { logAudit } from "../../services/audit.js";
import { pool } from "../../db/pool.js";

const pageSchema = z
  .object({
    spaceId: z.string().uuid(),
    title: z.string().min(3),
    slug: z.string().optional(),
    bodyMarkdown: z.string().min(1),
    excerpt: z.string().max(240).optional(),
    visibility: z.enum(["public", "internal", "restricted"]),
    state: z.enum(["draft", "published"]),
    parentPageId: z.string().uuid().nullable().optional(),
    tagNames: z.array(z.string()).optional(),
    allowedRoleKeys: z.array(z.enum(["owner", "admin", "editor", "viewer", "public"])).optional(),
    allowedGroupIds: z.array(z.string().uuid()).optional()
  })
  .superRefine((input, context) => {
    const permissionsCount =
      (input.allowedRoleKeys?.length ?? 0) + (input.allowedGroupIds?.length ?? 0);

    if (input.visibility === "restricted" && permissionsCount === 0) {
      context.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Restricted pages require at least one role or group permission",
        path: ["allowedRoleKeys"]
      });
    }
  });

export const pagesRouter = Router();

pagesRouter.get("/space/:spaceKey", async (req, res) => {
  const pages = await listPagesForSpace(req.params.spaceKey, req.user ?? null);
  return res.json({ pages });
});

pagesRouter.get("/drafts", requireEditor, async (req, res) => {
  const pages = await listDraftPages(req.user ?? null);
  return res.json({ pages });
});

pagesRouter.get("/slug/:slug", async (req, res) => {
  const page = await getPageBySlug(req.params.slug, req.user ?? null);
  if (!page) {
    return res.status(404).json({ error: "Page not found" });
  }
  return res.json({ page });
});

pagesRouter.post("/", requireEditor, async (req, res) => {
  const input = pageSchema.parse(req.body);
  const result = await createOrUpdatePage(null, input, req.user!.id);
  const workspace = await pool.query(`SELECT key FROM spaces WHERE id = $1`, [input.spaceId]);
  const actor = {
    id: req.user!.id,
    name: req.user!.displayName,
    email: req.user!.email
  };
  await logAudit(req.user!.id, "page.create", "page", result.pageId, { slug: result.slug });
  await enqueueWebhookEvent("page.created", { pageId: result.pageId, slug: result.slug }, {
    actor,
    workspaceId: workspace.rows[0]?.key ?? null
  });
  if (input.state === "published") {
    await enqueueWebhookEvent("page.published", { pageId: result.pageId, slug: result.slug }, {
      actor,
      workspaceId: workspace.rows[0]?.key ?? null
    });
  }
  return res.status(201).json(result);
});

pagesRouter.put("/:pageId", requireEditor, async (req, res) => {
  const input = pageSchema.parse(req.body);
  const result = await createOrUpdatePage(req.params.pageId, input, req.user!.id);
  const workspace = await pool.query(`SELECT key FROM spaces WHERE id = $1`, [input.spaceId]);
  await logAudit(req.user!.id, "page.update", "page", req.params.pageId, { slug: result.slug });
  await enqueueWebhookEvent("page.updated", { pageId: req.params.pageId, slug: result.slug }, {
    actor: {
      id: req.user!.id,
      name: req.user!.displayName,
      email: req.user!.email
    },
    workspaceId: workspace.rows[0]?.key ?? null
  });
  return res.json(result);
});

pagesRouter.get("/:pageId/revisions", async (req, res) => {
  const revisions = await listPageRevisions(req.params.pageId, req.user ?? null);
  if (!revisions) {
    return res.status(404).json({ error: "Page not found" });
  }
  return res.json({ revisions });
});

pagesRouter.post("/:pageId/revisions/:revisionId/rollback", requireEditor, async (req, res) => {
  const result = await rollbackPageRevision(req.params.pageId, req.params.revisionId, req.user!.id);
  if (!result) {
    return res.status(404).json({ error: "Revision not found" });
  }
  await logAudit(req.user!.id, "page.rollback", "page", req.params.pageId, {
    revisionId: req.params.revisionId
  });
  return res.json(result);
});

pagesRouter.delete("/:pageId", requireEditor, async (req, res) => {
  const existing = await pool.query(`SELECT slug, space_id FROM pages WHERE id = $1`, [req.params.pageId]);
  if (!existing.rowCount) {
    return res.status(404).json({ error: "Page not found" });
  }

  await pool.query(`DELETE FROM pages WHERE id = $1`, [req.params.pageId]);
  const workspace = await pool.query(`SELECT key FROM spaces WHERE id = $1`, [existing.rows[0].space_id]);
  await enqueueWebhookEvent("page.deleted", { pageId: req.params.pageId, slug: existing.rows[0].slug }, {
    actor: {
      id: req.user!.id,
      name: req.user!.displayName,
      email: req.user!.email
    },
    workspaceId: workspace.rows[0]?.key ?? null
  });
  await logAudit(req.user!.id, "page.delete", "page", req.params.pageId);
  return res.status(204).send();
});