public
anord
read
Ledger
Why work hard when you can work easier?
Languages
Repository composition by tracked source files.
TypeScript
86%
CSS
10%
SQL
3%
Shell
1%
HTML
0%
Trace
apps/api/src/http/routes/pages.ts
Trace helps you understand code history line by line. See who changed each line, when it changed, and which commit introduced it.
Author
Date
Commit
Line
Code
1
import { Router } from "express";
2
import { z } from "zod";
3
import { requireEditor } from "../middleware/auth.js";
4
import {
5
createOrUpdatePage,
6
getPageBySlug,
7
listPageRevisions,
8
listDraftPages,
9
listPagesForSpace,
10
rollbackPageRevision
11
} from "../../services/pages.js";
12
import { enqueueWebhookEvent } from "../../services/webhooks.js";
13
import { logAudit } from "../../services/audit.js";
14
import { pool } from "../../db/pool.js";
16
const pageSchema = z
17
.object({
18
spaceId: z.string().uuid(),
19
title: z.string().min(3),
20
slug: z.string().optional(),
21
bodyMarkdown: z.string().min(1),
22
excerpt: z.string().max(240).optional(),
23
visibility: z.enum(["public", "internal", "restricted"]),
24
state: z.enum(["draft", "published"]),
25
parentPageId: z.string().uuid().nullable().optional(),
26
tagNames: z.array(z.string()).optional(),
27
allowedRoleKeys: z.array(z.enum(["owner", "admin", "editor", "viewer", "public"])).optional(),
28
allowedGroupIds: z.array(z.string().uuid()).optional()
29
})
30
.superRefine((input, context) => {
31
const permissionsCount =
32
(input.allowedRoleKeys?.length ?? 0) + (input.allowedGroupIds?.length ?? 0);
34
if (input.visibility === "restricted" && permissionsCount === 0) {
35
context.addIssue({
36
code: z.ZodIssueCode.custom,
37
message: "Restricted pages require at least one role or group permission",
38
path: ["allowedRoleKeys"]
39
});
40
}
41
});
43
export const pagesRouter = Router();
45
pagesRouter.get("/space/:spaceKey", async (req, res) => {
46
const pages = await listPagesForSpace(req.params.spaceKey, req.user ?? null);
47
return res.json({ pages });
48
});
50
pagesRouter.get("/drafts", requireEditor, async (req, res) => {
51
const pages = await listDraftPages(req.user ?? null);
52
return res.json({ pages });
53
});
55
pagesRouter.get("/slug/:slug", async (req, res) => {
56
const page = await getPageBySlug(req.params.slug, req.user ?? null);
57
if (!page) {
58
return res.status(404).json({ error: "Page not found" });
59
}
60
return res.json({ page });
61
});
63
pagesRouter.post("/", requireEditor, async (req, res) => {
64
const input = pageSchema.parse(req.body);
65
const result = await createOrUpdatePage(null, input, req.user!.id);
66
const workspace = await pool.query(`SELECT key FROM spaces WHERE id = $1`, [input.spaceId]);
67
const actor = {
68
id: req.user!.id,
69
name: req.user!.displayName,
70
email: req.user!.email
71
};
72
await logAudit(req.user!.id, "page.create", "page", result.pageId, { slug: result.slug });
73
await enqueueWebhookEvent("page.created", { pageId: result.pageId, slug: result.slug }, {
74
actor,
75
workspaceId: workspace.rows[0]?.key ?? null
76
});
77
if (input.state === "published") {
78
await enqueueWebhookEvent("page.published", { pageId: result.pageId, slug: result.slug }, {
79
actor,
80
workspaceId: workspace.rows[0]?.key ?? null
81
});
82
}
83
return res.status(201).json(result);
84
});
86
pagesRouter.put("/:pageId", requireEditor, async (req, res) => {
87
const input = pageSchema.parse(req.body);
88
const result = await createOrUpdatePage(req.params.pageId, input, req.user!.id);
89
const workspace = await pool.query(`SELECT key FROM spaces WHERE id = $1`, [input.spaceId]);
90
await logAudit(req.user!.id, "page.update", "page", req.params.pageId, { slug: result.slug });
91
await enqueueWebhookEvent("page.updated", { pageId: req.params.pageId, slug: result.slug }, {
92
actor: {
93
id: req.user!.id,
94
name: req.user!.displayName,
95
email: req.user!.email
96
},
97
workspaceId: workspace.rows[0]?.key ?? null
98
});
99
return res.json(result);
100
});
102
pagesRouter.get("/:pageId/revisions", async (req, res) => {
103
const revisions = await listPageRevisions(req.params.pageId, req.user ?? null);
104
if (!revisions) {
105
return res.status(404).json({ error: "Page not found" });
106
}
107
return res.json({ revisions });
108
});
110
pagesRouter.post("/:pageId/revisions/:revisionId/rollback", requireEditor, async (req, res) => {
111
const result = await rollbackPageRevision(req.params.pageId, req.params.revisionId, req.user!.id);
112
if (!result) {
113
return res.status(404).json({ error: "Revision not found" });
114
}
115
await logAudit(req.user!.id, "page.rollback", "page", req.params.pageId, {
116
revisionId: req.params.revisionId
117
});
118
return res.json(result);
119
});
121
pagesRouter.delete("/:pageId", requireEditor, async (req, res) => {
122
const existing = await pool.query(`SELECT slug, space_id FROM pages WHERE id = $1`, [req.params.pageId]);
123
if (!existing.rowCount) {
124
return res.status(404).json({ error: "Page not found" });
125
}
127
await pool.query(`DELETE FROM pages WHERE id = $1`, [req.params.pageId]);
128
const workspace = await pool.query(`SELECT key FROM spaces WHERE id = $1`, [existing.rows[0].space_id]);
129
await enqueueWebhookEvent("page.deleted", { pageId: req.params.pageId, slug: existing.rows[0].slug }, {
130
actor: {
131
id: req.user!.id,
132
name: req.user!.displayName,
133
email: req.user!.email
134
},
135
workspaceId: workspace.rows[0]?.key ?? null
136
});
137
await logAudit(req.user!.id, "page.delete", "page", req.params.pageId);
138
return res.status(204).send();
139
});