this repo has no description
1import { Hono } from "hono";
2import { html } from "hono/html";
3import { layout } from "../views/layouts/main";
4import { requireAuth, type Session } from "../lib/session";
5import { csrfField } from "../lib/csrf";
6import type { AppVariables } from "../types";
7
8export const publicationRoutes = new Hono<{ Variables: AppVariables }>();
9
10const PUBLICATION_COLLECTION = "site.standard.publication";
11
12// View/manage publication
13publicationRoutes.get("/", async (c) => {
14 let session: Session;
15 try {
16 session = requireAuth(c);
17 } catch {
18 return c.redirect("/auth/login");
19 }
20
21 try {
22 // Fetch existing publication
23 const response = await session.agent!.com.atproto.repo.listRecords({
24 repo: session.did!,
25 collection: PUBLICATION_COLLECTION,
26 limit: 1,
27 });
28
29 const publication = response.data.records[0];
30
31 if (publication) {
32 const pub = publication.value as any;
33 const content = html`
34 <div class="publication">
35 <h1>Your Publication</h1>
36
37 <div class="pub-details">
38 <h2>${pub.name}</h2>
39 <p class="url">
40 <a href="${pub.url}" target="_blank">${pub.url}</a>
41 </p>
42 ${
43 pub.description
44 ? html`<p class="description">${pub.description}</p>`
45 : ""
46 }
47 </div>
48
49 <div class="actions">
50 <a href="/publication/edit" class="btn btn-primary"
51 >Edit Publication</a
52 >
53 </div>
54 </div>
55 `;
56 return c.html(
57 layout(content, { title: "Publication - sitebase", session }),
58 );
59 }
60
61 // No publication exists, show create form
62 return c.redirect("/publication/new");
63 } catch (error) {
64 console.error("Error fetching publication:", error);
65 return c.redirect("/publication/new");
66 }
67});
68
69// New publication form
70publicationRoutes.get("/new", async (c) => {
71 let session: Session;
72 try {
73 session = requireAuth(c);
74 } catch {
75 return c.redirect("/auth/login");
76 }
77
78 const csrfToken = c.get("csrfToken") as string;
79
80 const content = html`
81 <div class="form-page">
82 <h1>Create Publication</h1>
83
84 <form action="/publication/new" method="POST">
85 ${csrfField(csrfToken)}
86 <div class="form-group">
87 <label for="name">Name *</label>
88 <input type="text" id="name" name="name" required maxlength="128" />
89 </div>
90
91 <div class="form-group">
92 <label for="url">URL *</label>
93 <input
94 type="url"
95 id="url"
96 name="url"
97 placeholder="https://yourblog.com"
98 required
99 />
100 <small
101 >The base URL of your publication (without trailing slash)</small
102 >
103 </div>
104
105 <div class="form-group">
106 <label for="description">Description</label>
107 <textarea
108 id="description"
109 name="description"
110 rows="3"
111 maxlength="300"
112 ></textarea>
113 </div>
114
115 <button type="submit" class="btn btn-primary">
116 Create Publication
117 </button>
118 </form>
119 </div>
120 `;
121
122 return c.html(
123 layout(content, { title: "New Publication - sitebase", session }),
124 );
125});
126
127// Handle publication creation
128publicationRoutes.post("/new", async (c) => {
129 let session: Session;
130 try {
131 session = requireAuth(c);
132 } catch {
133 return c.redirect("/auth/login");
134 }
135
136 const body = await c.req.parseBody();
137 const name = body.name as string;
138 const url = (body.url as string).replace(/\/$/, ""); // Remove trailing slash
139 const description = (body.description as string) || undefined;
140
141 try {
142 // Generate a TID for the record key
143 const rkey = generateTID();
144
145 await session.agent!.com.atproto.repo.createRecord({
146 repo: session.did!,
147 collection: PUBLICATION_COLLECTION,
148 rkey,
149 record: {
150 $type: PUBLICATION_COLLECTION,
151 name,
152 url,
153 ...(description && { description }),
154 },
155 });
156
157 return c.redirect("/publication");
158 } catch (error) {
159 console.error("Error creating publication:", error);
160 return c.redirect("/publication/new?error=create_failed");
161 }
162});
163
164// Edit publication form
165publicationRoutes.get("/edit", async (c) => {
166 let session: Session;
167 try {
168 session = requireAuth(c);
169 } catch {
170 return c.redirect("/auth/login");
171 }
172
173 try {
174 const response = await session.agent!.com.atproto.repo.listRecords({
175 repo: session.did!,
176 collection: PUBLICATION_COLLECTION,
177 limit: 1,
178 });
179
180 const publication = response.data.records[0];
181 if (!publication) {
182 return c.redirect("/publication/new");
183 }
184
185 const pub = publication.value as any;
186 const rkey = publication.uri.split("/").pop();
187
188 const csrfToken = c.get("csrfToken") as string;
189
190 const content = html`
191 <div class="form-page">
192 <h1>Edit Publication</h1>
193
194 <form action="/publication/edit" method="POST">
195 ${csrfField(csrfToken)}
196 <input type="hidden" name="rkey" value="${rkey}" />
197
198 <div class="form-group">
199 <label for="name">Name *</label>
200 <input
201 type="text"
202 id="name"
203 name="name"
204 value="${pub.name}"
205 required
206 maxlength="128"
207 />
208 </div>
209
210 <div class="form-group">
211 <label for="url">URL *</label>
212 <input type="url" id="url" name="url" value="${pub.url}" required />
213 </div>
214
215 <div class="form-group">
216 <label for="description">Description</label>
217 <textarea
218 id="description"
219 name="description"
220 rows="3"
221 maxlength="300"
222 >
223${pub.description || ""}</textarea
224 >
225 </div>
226
227 <button type="submit" class="btn btn-primary">Save Changes</button>
228 <a href="/publication" class="btn btn-secondary">Cancel</a>
229 </form>
230 </div>
231 `;
232
233 return c.html(
234 layout(content, { title: "Edit Publication - sitebase", session }),
235 );
236 } catch (error) {
237 console.error("Error fetching publication:", error);
238 return c.redirect("/publication");
239 }
240});
241
242// Handle publication update
243publicationRoutes.post("/edit", async (c) => {
244 let session: Session;
245 try {
246 session = requireAuth(c);
247 } catch {
248 return c.redirect("/auth/login");
249 }
250
251 const body = await c.req.parseBody();
252 const rkey = body.rkey as string;
253 const name = body.name as string;
254 const url = (body.url as string).replace(/\/$/, "");
255 const description = (body.description as string) || undefined;
256
257 try {
258 await session.agent!.com.atproto.repo.putRecord({
259 repo: session.did!,
260 collection: PUBLICATION_COLLECTION,
261 rkey,
262 record: {
263 $type: PUBLICATION_COLLECTION,
264 name,
265 url,
266 ...(description && { description }),
267 },
268 });
269
270 return c.redirect("/publication");
271 } catch (error) {
272 console.error("Error updating publication:", error);
273 return c.redirect("/publication/edit?error=update_failed");
274 }
275});
276
277// Generate a TID (timestamp-based ID)
278function generateTID(): string {
279 const now = Date.now() * 1000; // microseconds
280 const clockId = Math.floor(Math.random() * 1024);
281 const tid = (BigInt(now) << 10n) | BigInt(clockId);
282 return tid.toString(36).padStart(13, "0");
283}