@geekity/cms 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +870 -0
- package/admin/layouts/_flash.njk +5 -0
- package/admin/layouts/base.njk +27 -0
- package/admin/layouts/dashboard.njk +78 -0
- package/admin/layouts/document-conflict.njk +49 -0
- package/admin/layouts/document-editor.njk +167 -0
- package/admin/layouts/document-list.njk +92 -0
- package/admin/layouts/federation.njk +145 -0
- package/admin/layouts/login.njk +23 -0
- package/admin/layouts/placeholder.njk +10 -0
- package/admin/layouts/settings.njk +116 -0
- package/admin/layouts/setup.njk +27 -0
- package/admin/layouts/shell.njk +37 -0
- package/admin/layouts/users.njk +91 -0
- package/admin/static/admin.css +726 -0
- package/admin/static/editor.js +30 -0
- package/dist/admin/assets.d.ts +26 -0
- package/dist/admin/assets.d.ts.map +1 -0
- package/dist/admin/assets.js +38 -0
- package/dist/admin/assets.js.map +1 -0
- package/dist/admin/credentials.d.ts +28 -0
- package/dist/admin/credentials.d.ts.map +1 -0
- package/dist/admin/credentials.js +39 -0
- package/dist/admin/credentials.js.map +1 -0
- package/dist/admin/documents.d.ts +134 -0
- package/dist/admin/documents.d.ts.map +1 -0
- package/dist/admin/documents.js +613 -0
- package/dist/admin/documents.js.map +1 -0
- package/dist/admin/federation.d.ts +194 -0
- package/dist/admin/federation.d.ts.map +1 -0
- package/dist/admin/federation.js +299 -0
- package/dist/admin/federation.js.map +1 -0
- package/dist/admin/flash.d.ts +24 -0
- package/dist/admin/flash.d.ts.map +1 -0
- package/dist/admin/flash.js +31 -0
- package/dist/admin/flash.js.map +1 -0
- package/dist/admin/index.d.ts +23 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +15 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/passwords.d.ts +49 -0
- package/dist/admin/passwords.d.ts.map +1 -0
- package/dist/admin/passwords.js +122 -0
- package/dist/admin/passwords.js.map +1 -0
- package/dist/admin/preview.d.ts +18 -0
- package/dist/admin/preview.d.ts.map +1 -0
- package/dist/admin/preview.js +83 -0
- package/dist/admin/preview.js.map +1 -0
- package/dist/admin/routes.d.ts +54 -0
- package/dist/admin/routes.d.ts.map +1 -0
- package/dist/admin/routes.js +383 -0
- package/dist/admin/routes.js.map +1 -0
- package/dist/admin/session.d.ts +44 -0
- package/dist/admin/session.d.ts.map +1 -0
- package/dist/admin/session.js +69 -0
- package/dist/admin/session.js.map +1 -0
- package/dist/admin/settings.d.ts +194 -0
- package/dist/admin/settings.d.ts.map +1 -0
- package/dist/admin/settings.js +470 -0
- package/dist/admin/settings.js.map +1 -0
- package/dist/admin/store.d.ts +407 -0
- package/dist/admin/store.d.ts.map +1 -0
- package/dist/admin/store.js +704 -0
- package/dist/admin/store.js.map +1 -0
- package/dist/admin/templates.d.ts +36 -0
- package/dist/admin/templates.d.ts.map +1 -0
- package/dist/admin/templates.js +43 -0
- package/dist/admin/templates.js.map +1 -0
- package/dist/admin/uploads.d.ts +96 -0
- package/dist/admin/uploads.d.ts.map +1 -0
- package/dist/admin/uploads.js +179 -0
- package/dist/admin/uploads.js.map +1 -0
- package/dist/admin/users.d.ts +95 -0
- package/dist/admin/users.d.ts.map +1 -0
- package/dist/admin/users.js +233 -0
- package/dist/admin/users.js.map +1 -0
- package/dist/cli.d.ts +85 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +424 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +172 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +184 -0
- package/dist/config.js.map +1 -0
- package/dist/content/document.d.ts +66 -0
- package/dist/content/document.d.ts.map +1 -0
- package/dist/content/document.js +13 -0
- package/dist/content/document.js.map +1 -0
- package/dist/content/index.d.ts +17 -0
- package/dist/content/index.d.ts.map +1 -0
- package/dist/content/index.js +10 -0
- package/dist/content/index.js.map +1 -0
- package/dist/content/markdown.d.ts +3 -0
- package/dist/content/markdown.d.ts.map +1 -0
- package/dist/content/markdown.js +39 -0
- package/dist/content/markdown.js.map +1 -0
- package/dist/content/media.d.ts +65 -0
- package/dist/content/media.d.ts.map +1 -0
- package/dist/content/media.js +143 -0
- package/dist/content/media.js.map +1 -0
- package/dist/content/parser.d.ts +24 -0
- package/dist/content/parser.d.ts.map +1 -0
- package/dist/content/parser.js +165 -0
- package/dist/content/parser.js.map +1 -0
- package/dist/content/save.d.ts +60 -0
- package/dist/content/save.d.ts.map +1 -0
- package/dist/content/save.js +103 -0
- package/dist/content/save.js.map +1 -0
- package/dist/content/slug.d.ts +25 -0
- package/dist/content/slug.d.ts.map +1 -0
- package/dist/content/slug.js +75 -0
- package/dist/content/slug.js.map +1 -0
- package/dist/content/store.d.ts +133 -0
- package/dist/content/store.d.ts.map +1 -0
- package/dist/content/store.js +411 -0
- package/dist/content/store.js.map +1 -0
- package/dist/content/sync.d.ts +146 -0
- package/dist/content/sync.d.ts.map +1 -0
- package/dist/content/sync.js +421 -0
- package/dist/content/sync.js.map +1 -0
- package/dist/content/writer.d.ts +37 -0
- package/dist/content/writer.d.ts.map +1 -0
- package/dist/content/writer.js +97 -0
- package/dist/content/writer.js.map +1 -0
- package/dist/env.d.ts +48 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +2 -0
- package/dist/env.js.map +1 -0
- package/dist/federation/actor.d.ts +55 -0
- package/dist/federation/actor.d.ts.map +1 -0
- package/dist/federation/actor.js +101 -0
- package/dist/federation/actor.js.map +1 -0
- package/dist/federation/article.d.ts +83 -0
- package/dist/federation/article.d.ts.map +1 -0
- package/dist/federation/article.js +169 -0
- package/dist/federation/article.js.map +1 -0
- package/dist/federation/delivery.d.ts +90 -0
- package/dist/federation/delivery.d.ts.map +1 -0
- package/dist/federation/delivery.js +289 -0
- package/dist/federation/delivery.js.map +1 -0
- package/dist/federation/federation.d.ts +75 -0
- package/dist/federation/federation.d.ts.map +1 -0
- package/dist/federation/federation.js +190 -0
- package/dist/federation/federation.js.map +1 -0
- package/dist/federation/followers.d.ts +43 -0
- package/dist/federation/followers.d.ts.map +1 -0
- package/dist/federation/followers.js +69 -0
- package/dist/federation/followers.js.map +1 -0
- package/dist/federation/inbox.d.ts +68 -0
- package/dist/federation/inbox.d.ts.map +1 -0
- package/dist/federation/inbox.js +171 -0
- package/dist/federation/inbox.js.map +1 -0
- package/dist/federation/index.d.ts +14 -0
- package/dist/federation/index.d.ts.map +1 -0
- package/dist/federation/index.js +10 -0
- package/dist/federation/index.js.map +1 -0
- package/dist/federation/keys.d.ts +27 -0
- package/dist/federation/keys.d.ts.map +1 -0
- package/dist/federation/keys.js +60 -0
- package/dist/federation/keys.js.map +1 -0
- package/dist/federation/mount.d.ts +23 -0
- package/dist/federation/mount.d.ts.map +1 -0
- package/dist/federation/mount.js +71 -0
- package/dist/federation/mount.js.map +1 -0
- package/dist/federation/paths.d.ts +85 -0
- package/dist/federation/paths.d.ts.map +1 -0
- package/dist/federation/paths.js +97 -0
- package/dist/federation/paths.js.map +1 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +71 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +125 -0
- package/dist/init.js.map +1 -0
- package/dist/web/assets.d.ts +65 -0
- package/dist/web/assets.d.ts.map +1 -0
- package/dist/web/assets.js +152 -0
- package/dist/web/assets.js.map +1 -0
- package/dist/web/context.d.ts +121 -0
- package/dist/web/context.d.ts.map +1 -0
- package/dist/web/context.js +112 -0
- package/dist/web/context.js.map +1 -0
- package/dist/web/documents.d.ts +28 -0
- package/dist/web/documents.d.ts.map +1 -0
- package/dist/web/documents.js +39 -0
- package/dist/web/documents.js.map +1 -0
- package/dist/web/feeds.d.ts +122 -0
- package/dist/web/feeds.d.ts.map +1 -0
- package/dist/web/feeds.js +229 -0
- package/dist/web/feeds.js.map +1 -0
- package/dist/web/index.d.ts +17 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +10 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/negotiate.d.ts +219 -0
- package/dist/web/negotiate.d.ts.map +1 -0
- package/dist/web/negotiate.js +409 -0
- package/dist/web/negotiate.js.map +1 -0
- package/dist/web/pagination.d.ts +48 -0
- package/dist/web/pagination.d.ts.map +1 -0
- package/dist/web/pagination.js +30 -0
- package/dist/web/pagination.js.map +1 -0
- package/dist/web/render.d.ts +72 -0
- package/dist/web/render.d.ts.map +1 -0
- package/dist/web/render.js +73 -0
- package/dist/web/render.js.map +1 -0
- package/dist/web/routes.d.ts +31 -0
- package/dist/web/routes.d.ts.map +1 -0
- package/dist/web/routes.js +366 -0
- package/dist/web/routes.js.map +1 -0
- package/dist/web/templates.d.ts +34 -0
- package/dist/web/templates.d.ts.map +1 -0
- package/dist/web/templates.js +106 -0
- package/dist/web/templates.js.map +1 -0
- package/package.json +68 -0
- package/templates/site/content/_data/site.json +8 -0
- package/templates/site/content/pages/about.md +11 -0
- package/templates/site/content/pages/pages.json +3 -0
- package/templates/site/content/posts/2026-01-01-hello-world.md +34 -0
- package/templates/site/content/posts/posts.json +4 -0
- package/templates/site/geekity.config.ts +11 -0
- package/templates/site/gitignore +3 -0
- package/templates/site/server.ts +20 -0
- package/templates/site/tsconfig.json +18 -0
- package/themes/default/README.md +148 -0
- package/themes/default/layouts/404.njk +9 -0
- package/themes/default/layouts/base.njk +59 -0
- package/themes/default/layouts/home.njk +9 -0
- package/themes/default/layouts/page.njk +13 -0
- package/themes/default/layouts/post.njk +30 -0
- package/themes/default/layouts/tag.njk +19 -0
- package/themes/default/partials/pagination.njk +14 -0
- package/themes/default/partials/post-list.njk +24 -0
- package/themes/default/partials/tags.njk +12 -0
- package/themes/default/static/style.css +327 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { contentFilePath, freeSlug, saveDocument } from '../content/save.js';
|
|
2
|
+
import { defaultPermalink, slugify } from '../content/slug.js';
|
|
3
|
+
import { normalizeBody } from '../content/writer.js';
|
|
4
|
+
import { adminAssetResponse, ADMIN_ASSET_PREFIX } from './assets.js';
|
|
5
|
+
import { credentialProblem } from './credentials.js';
|
|
6
|
+
import { editorPath, mountDocumentScreens, PAGE_KIND, POST_KIND } from './documents.js';
|
|
7
|
+
import { FEDERATION_PATH, mountFederationScreen } from './federation.js';
|
|
8
|
+
import { flash, takeFlash } from './flash.js';
|
|
9
|
+
import { mountPreview } from './preview.js';
|
|
10
|
+
import { AVATAR_PATH, mountSettings } from './settings.js';
|
|
11
|
+
import { ADMIN_PREFIX, clearSessionCookie, CSRF_FIELD, csrfTokenMatches, sessionIdFrom, setSessionCookie, } from './session.js';
|
|
12
|
+
import { DuplicateUsernameError } from './store.js';
|
|
13
|
+
import { ADMIN_TEMPLATES, createAdminTemplateEnvironment } from './templates.js';
|
|
14
|
+
import { mountUploads, refuseOversizedUpload, UPLOADS_PATH } from './uploads.js';
|
|
15
|
+
import { mountUsers } from './users.js';
|
|
16
|
+
/** Where the login form lives. */
|
|
17
|
+
export const LOGIN_PATH = `${ADMIN_PREFIX}/login`;
|
|
18
|
+
/** Where the first-run form lives. */
|
|
19
|
+
export const SETUP_PATH = `${ADMIN_PREFIX}/setup`;
|
|
20
|
+
/** Where the logout form posts. */
|
|
21
|
+
export const LOGOUT_PATH = `${ADMIN_PREFIX}/logout`;
|
|
22
|
+
/** Where the dashboard's quick draft form posts. */
|
|
23
|
+
export const QUICK_DRAFT_PATH = `${ADMIN_PREFIX}/quick-draft`;
|
|
24
|
+
/**
|
|
25
|
+
* The sections of the admin, in the order doc-5 lists them.
|
|
26
|
+
*
|
|
27
|
+
* Every screen renders the same list and marks one of them, so a screen added
|
|
28
|
+
* later only has to name its section. The screens behind most of these links
|
|
29
|
+
* are still placeholders; the navigation is the shape of the whole admin, not
|
|
30
|
+
* of the part that is built.
|
|
31
|
+
*/
|
|
32
|
+
export const ADMIN_SECTIONS = [
|
|
33
|
+
{ section: 'dashboard', label: 'Dashboard', url: ADMIN_PREFIX },
|
|
34
|
+
{ section: 'posts', label: 'Posts', url: `${ADMIN_PREFIX}/posts` },
|
|
35
|
+
{ section: 'pages', label: 'Pages', url: `${ADMIN_PREFIX}/pages` },
|
|
36
|
+
{ section: 'settings', label: 'Settings', url: `${ADMIN_PREFIX}/settings` },
|
|
37
|
+
{ section: 'users', label: 'Users', url: `${ADMIN_PREFIX}/users` },
|
|
38
|
+
{ section: 'federation', label: 'Federation', url: `${ADMIN_PREFIX}/federation` },
|
|
39
|
+
];
|
|
40
|
+
/** How many recent posts the dashboard lists. */
|
|
41
|
+
export const DASHBOARD_RECENT_POSTS = 5;
|
|
42
|
+
/** Where the editor for a post lives. */
|
|
43
|
+
export function postEditorPath(slug) {
|
|
44
|
+
return editorPath(POST_KIND, slug);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Register the admin on a Hono app.
|
|
48
|
+
*
|
|
49
|
+
* Everything under `/admin` goes through one guard, registered as middleware
|
|
50
|
+
* rather than per route, so a path with no handler of its own — `/admin/posts`
|
|
51
|
+
* before TASK-11 builds it — still redirects an anonymous visitor to the login
|
|
52
|
+
* form instead of falling through to the public site's 404.
|
|
53
|
+
*
|
|
54
|
+
* The guard decides three things, in order: whether the site has any users at
|
|
55
|
+
* all (none means every admin URL is the setup form), whether the request
|
|
56
|
+
* carries a live session, and, for anything but a GET, whether it carries this
|
|
57
|
+
* session's CSRF token.
|
|
58
|
+
*/
|
|
59
|
+
export function mountAdmin(app) {
|
|
60
|
+
// Built on the first request, because the config that says whether to cache
|
|
61
|
+
// templates is only on the context.
|
|
62
|
+
let environment;
|
|
63
|
+
function templates(c) {
|
|
64
|
+
environment ??= createAdminTemplateEnvironment({ noCache: c.var.config.watch });
|
|
65
|
+
return environment;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Render one admin template.
|
|
69
|
+
*
|
|
70
|
+
* Everything the chrome needs — who is signed in, the navigation, the CSRF
|
|
71
|
+
* token, the queued flash messages — is put in the context here rather than
|
|
72
|
+
* by each handler, so a new screen is a template and a `section` name.
|
|
73
|
+
* Reading the flash is what clears it, so it shows on exactly this page.
|
|
74
|
+
*/
|
|
75
|
+
function render(c, template, context = {}) {
|
|
76
|
+
const session = c.var.session;
|
|
77
|
+
const userId = session?.userId ?? null;
|
|
78
|
+
const html = templates(c).render(template, {
|
|
79
|
+
site: c.var.renderer.site(),
|
|
80
|
+
adminUrl: ADMIN_PREFIX,
|
|
81
|
+
siteUrl: '/',
|
|
82
|
+
assetPrefix: ADMIN_ASSET_PREFIX,
|
|
83
|
+
navigation: ADMIN_SECTIONS,
|
|
84
|
+
logoutUrl: LOGOUT_PATH,
|
|
85
|
+
csrfToken: session?.csrfToken ?? '',
|
|
86
|
+
user: userId === null ? undefined : c.var.admin.getUserById(userId),
|
|
87
|
+
flash: takeFlash(c),
|
|
88
|
+
...context,
|
|
89
|
+
});
|
|
90
|
+
return c.html(html);
|
|
91
|
+
}
|
|
92
|
+
// Registered before the guard, so the login page can load its stylesheet
|
|
93
|
+
// while nobody is logged in. Nothing under it is secret.
|
|
94
|
+
app.get(`${ADMIN_ASSET_PREFIX}*`, (c) => {
|
|
95
|
+
const pathname = new URL(c.req.url).pathname.slice(ADMIN_ASSET_PREFIX.length);
|
|
96
|
+
const response = adminAssetResponse(decodePath(pathname), c.req.header('if-none-match'));
|
|
97
|
+
return response ?? c.notFound();
|
|
98
|
+
});
|
|
99
|
+
// In front of the guard, because the guard parses the form to find the CSRF
|
|
100
|
+
// token and parsing a multipart form reads the whole file into memory. Both
|
|
101
|
+
// multipart endpoints need it: the editor's uploads and the avatar's.
|
|
102
|
+
app.use(UPLOADS_PATH, refuseOversizedUpload);
|
|
103
|
+
app.use(AVATAR_PATH, refuseOversizedUpload);
|
|
104
|
+
app.use(ADMIN_PREFIX, guard);
|
|
105
|
+
app.use(`${ADMIN_PREFIX}/*`, guard);
|
|
106
|
+
app.get(SETUP_PATH, (c) => {
|
|
107
|
+
anonymousSession(c);
|
|
108
|
+
return render(c, ADMIN_TEMPLATES.setup, { setupUrl: SETUP_PATH, username: '' });
|
|
109
|
+
});
|
|
110
|
+
app.post(SETUP_PATH, async (c) => {
|
|
111
|
+
const body = await c.req.parseBody();
|
|
112
|
+
const username = text(body[USERNAME_FIELD]).trim();
|
|
113
|
+
const password = text(body[PASSWORD_FIELD]);
|
|
114
|
+
const confirmation = text(body['password_confirmation']);
|
|
115
|
+
const problem = setupProblem(username, password, confirmation);
|
|
116
|
+
if (problem !== undefined)
|
|
117
|
+
return refuseSetup(c, username, problem);
|
|
118
|
+
let user;
|
|
119
|
+
try {
|
|
120
|
+
user = c.var.admin.createUser({ username, password });
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error instanceof DuplicateUsernameError) {
|
|
124
|
+
return refuseSetup(c, username, error.message);
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
// The first admin is logged straight in; making an account and then being
|
|
129
|
+
// shown a login form would be a pointless second step.
|
|
130
|
+
return logIn(c, user);
|
|
131
|
+
});
|
|
132
|
+
app.get(LOGIN_PATH, (c) => {
|
|
133
|
+
anonymousSession(c);
|
|
134
|
+
return render(c, ADMIN_TEMPLATES.login, { loginUrl: LOGIN_PATH, username: '' });
|
|
135
|
+
});
|
|
136
|
+
app.post(LOGIN_PATH, async (c) => {
|
|
137
|
+
const body = await c.req.parseBody();
|
|
138
|
+
const username = text(body[USERNAME_FIELD]).trim();
|
|
139
|
+
const password = text(body[PASSWORD_FIELD]);
|
|
140
|
+
const user = c.var.admin.verifyPassword(username, password);
|
|
141
|
+
if (user === undefined) {
|
|
142
|
+
// One message for both failures, so the form cannot be used to find out
|
|
143
|
+
// which usernames exist.
|
|
144
|
+
anonymousSession(c);
|
|
145
|
+
c.status(401);
|
|
146
|
+
return render(c, ADMIN_TEMPLATES.login, {
|
|
147
|
+
loginUrl: LOGIN_PATH,
|
|
148
|
+
username,
|
|
149
|
+
error: 'That username and password do not match.',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return logIn(c, user);
|
|
153
|
+
});
|
|
154
|
+
app.post(LOGOUT_PATH, (c) => {
|
|
155
|
+
const session = c.var.session;
|
|
156
|
+
if (session !== undefined)
|
|
157
|
+
c.var.admin.deleteSession(session.id);
|
|
158
|
+
clearSessionCookie(c, c.var.config);
|
|
159
|
+
return c.redirect(LOGIN_PATH, 303);
|
|
160
|
+
});
|
|
161
|
+
app.get(ADMIN_PREFIX, (c) => {
|
|
162
|
+
const store = c.var.store;
|
|
163
|
+
return render(c, ADMIN_TEMPLATES.dashboard, {
|
|
164
|
+
section: 'dashboard',
|
|
165
|
+
counts: store.counts(),
|
|
166
|
+
recent: store.listAll({ type: 'post', limit: DASHBOARD_RECENT_POSTS }).map((document) => ({
|
|
167
|
+
title: document.title,
|
|
168
|
+
slug: document.slug,
|
|
169
|
+
date: document.date,
|
|
170
|
+
draft: document.draft,
|
|
171
|
+
editUrl: postEditorPath(document.slug),
|
|
172
|
+
})),
|
|
173
|
+
quickDraftUrl: QUICK_DRAFT_PATH,
|
|
174
|
+
// The one number on the dashboard that is not about the content
|
|
175
|
+
// directory: it is what says whether publishing is reaching anybody.
|
|
176
|
+
followers: c.var.admin.countFollowers(),
|
|
177
|
+
federationUrl: FEDERATION_PATH,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
/**
|
|
181
|
+
* The dashboard's quick draft: a title and a body, straight to a draft file.
|
|
182
|
+
*
|
|
183
|
+
* It writes a draft rather than a published post because the point of the box
|
|
184
|
+
* is to catch an idea before it goes, and because the editor TASK-11 builds is
|
|
185
|
+
* where the decision to publish belongs.
|
|
186
|
+
*/
|
|
187
|
+
app.post(QUICK_DRAFT_PATH, async (c) => {
|
|
188
|
+
const body = await c.req.parseBody();
|
|
189
|
+
const title = text(body['title']).trim();
|
|
190
|
+
if (title === '') {
|
|
191
|
+
flash(c, 'error', 'A draft needs a title.');
|
|
192
|
+
return c.redirect(ADMIN_PREFIX, 303);
|
|
193
|
+
}
|
|
194
|
+
const contentDir = c.var.config.contentDir;
|
|
195
|
+
const store = c.var.store;
|
|
196
|
+
const date = new Date().toISOString();
|
|
197
|
+
const slug = await freeSlug({
|
|
198
|
+
contentDir,
|
|
199
|
+
store,
|
|
200
|
+
type: 'post',
|
|
201
|
+
slug: slugify(title) || 'untitled',
|
|
202
|
+
date,
|
|
203
|
+
});
|
|
204
|
+
const document = await saveDocument({
|
|
205
|
+
contentDir,
|
|
206
|
+
store,
|
|
207
|
+
path: contentFilePath({ type: 'post', slug, date }),
|
|
208
|
+
content: {
|
|
209
|
+
title,
|
|
210
|
+
date,
|
|
211
|
+
permalink: defaultPermalink({ type: 'post', slug, date }),
|
|
212
|
+
tags: [],
|
|
213
|
+
draft: true,
|
|
214
|
+
extra: {},
|
|
215
|
+
body: normalizeBody(text(body['body'])),
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
await c.var.announce({
|
|
219
|
+
type: 'created',
|
|
220
|
+
path: document.path,
|
|
221
|
+
previous: undefined,
|
|
222
|
+
next: document,
|
|
223
|
+
origin: 'admin',
|
|
224
|
+
});
|
|
225
|
+
flash(c, 'notice', `Draft saved: ${document.title}`);
|
|
226
|
+
return c.redirect(postEditorPath(document.slug), 303);
|
|
227
|
+
});
|
|
228
|
+
// The listing and the editor. Posts and pages are the same screens over the
|
|
229
|
+
// same store; the kind decides what a file is called and which fields the
|
|
230
|
+
// form has.
|
|
231
|
+
mountDocumentScreens(app, { kind: POST_KIND, render });
|
|
232
|
+
mountDocumentScreens(app, { kind: PAGE_KIND, render });
|
|
233
|
+
// The two endpoints the editor talks to rather than navigates to. Both are
|
|
234
|
+
// inside the guard, so both need the session's CSRF token like every other
|
|
235
|
+
// POST in the admin.
|
|
236
|
+
mountPreview(app);
|
|
237
|
+
mountUploads(app);
|
|
238
|
+
// The site's own settings: SQLite is the source, content/_data/site.json is
|
|
239
|
+
// the mirror an Eleventy build of the same content reads.
|
|
240
|
+
mountSettings(app, { render });
|
|
241
|
+
// Who may sign in: the list, the add form, and the change-password form for
|
|
242
|
+
// whoever is looking at it.
|
|
243
|
+
mountUsers(app, { render });
|
|
244
|
+
// The fediverse side: the actor, the followers, the inbox log, and what the
|
|
245
|
+
// site sent to whom.
|
|
246
|
+
mountFederationScreen(app, { render });
|
|
247
|
+
const built = new Set([
|
|
248
|
+
POST_KIND.section,
|
|
249
|
+
PAGE_KIND.section,
|
|
250
|
+
'dashboard',
|
|
251
|
+
'settings',
|
|
252
|
+
'users',
|
|
253
|
+
'federation',
|
|
254
|
+
]);
|
|
255
|
+
// The sections doc-5 lists but no task has built yet. They are registered so
|
|
256
|
+
// the navigation goes somewhere: a link that 404s reads as a broken admin,
|
|
257
|
+
// and the guard already keeps strangers out of all of them.
|
|
258
|
+
for (const item of ADMIN_SECTIONS) {
|
|
259
|
+
if (built.has(item.section))
|
|
260
|
+
continue;
|
|
261
|
+
app.get(item.url, (c) => render(c, ADMIN_TEMPLATES.placeholder, { section: item.section, heading: item.label }));
|
|
262
|
+
}
|
|
263
|
+
// `/admin/` is the same screen as `/admin`, and only one of them is the URL.
|
|
264
|
+
app.get(`${ADMIN_PREFIX}/`, (c) => c.redirect(ADMIN_PREFIX, 301));
|
|
265
|
+
/** The session to hang a CSRF token on, made on the spot if there is none. */
|
|
266
|
+
function anonymousSession(c) {
|
|
267
|
+
const existing = c.var.session;
|
|
268
|
+
if (existing !== undefined)
|
|
269
|
+
return existing;
|
|
270
|
+
const session = c.var.admin.createSession({
|
|
271
|
+
userId: null,
|
|
272
|
+
lifetimeSeconds: c.var.config.sessionLifetime,
|
|
273
|
+
});
|
|
274
|
+
setSessionCookie(c, session.id, { config: c.var.config, expiresAt: session.expiresAt });
|
|
275
|
+
c.set('session', session);
|
|
276
|
+
return session;
|
|
277
|
+
}
|
|
278
|
+
/** Re-render the setup form with a message, and the status a bad form gets. */
|
|
279
|
+
function refuseSetup(c, username, error) {
|
|
280
|
+
anonymousSession(c);
|
|
281
|
+
c.status(400);
|
|
282
|
+
return render(c, ADMIN_TEMPLATES.setup, { setupUrl: SETUP_PATH, username, error });
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Start an authenticated session and send the browser to the dashboard.
|
|
286
|
+
*
|
|
287
|
+
* The session the request arrived with is deleted rather than promoted, so a
|
|
288
|
+
* session id an attacker planted before login is not the id that ends up
|
|
289
|
+
* logged in.
|
|
290
|
+
*/
|
|
291
|
+
function logIn(c, user) {
|
|
292
|
+
const admin = c.var.admin;
|
|
293
|
+
const previous = c.var.session;
|
|
294
|
+
if (previous !== undefined)
|
|
295
|
+
admin.deleteSession(previous.id);
|
|
296
|
+
// Logging in is rare and cheap; it is a good moment to sweep the table.
|
|
297
|
+
admin.pruneSessions();
|
|
298
|
+
const session = admin.createSession({
|
|
299
|
+
userId: user.id,
|
|
300
|
+
lifetimeSeconds: c.var.config.sessionLifetime,
|
|
301
|
+
});
|
|
302
|
+
setSessionCookie(c, session.id, { config: c.var.config, expiresAt: session.expiresAt });
|
|
303
|
+
c.set('session', session);
|
|
304
|
+
return c.redirect(ADMIN_PREFIX, 303);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const USERNAME_FIELD = 'username';
|
|
308
|
+
const PASSWORD_FIELD = 'password';
|
|
309
|
+
/**
|
|
310
|
+
* The one gate in front of every admin URL.
|
|
311
|
+
*
|
|
312
|
+
* It is exported so a site that mounts its own admin routes can put them
|
|
313
|
+
* behind the same guard, and so the ordering is testable.
|
|
314
|
+
*/
|
|
315
|
+
export const guard = async (c, next) => {
|
|
316
|
+
const admin = c.var.admin;
|
|
317
|
+
const pathname = new URL(c.req.url).pathname;
|
|
318
|
+
// Reading a session prunes it when it has expired, so an expired session is
|
|
319
|
+
// gone from here on, not merely ignored.
|
|
320
|
+
const sessionId = sessionIdFrom(c);
|
|
321
|
+
const session = sessionId === undefined ? undefined : admin.getSession(sessionId);
|
|
322
|
+
c.set('session', session);
|
|
323
|
+
const isSetup = pathname === SETUP_PATH;
|
|
324
|
+
const isLogin = pathname === LOGIN_PATH;
|
|
325
|
+
if (admin.countUsers() === 0) {
|
|
326
|
+
// Nobody can log in yet, so there is exactly one thing to do here.
|
|
327
|
+
if (!isSetup)
|
|
328
|
+
return c.redirect(SETUP_PATH, 302);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// Setup is over. Offering the form again would be an open door.
|
|
332
|
+
if (isSetup)
|
|
333
|
+
return c.redirect(session?.userId == null ? LOGIN_PATH : ADMIN_PREFIX, 302);
|
|
334
|
+
if (!isLogin && (session === undefined || session.userId === null)) {
|
|
335
|
+
return c.redirect(LOGIN_PATH, 302);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (c.req.method !== 'GET' && c.req.method !== 'HEAD') {
|
|
339
|
+
if (session === undefined || !csrfTokenMatches(session.csrfToken, await submittedToken(c))) {
|
|
340
|
+
return c.text('That form was stale or came from somewhere else. Reload and try again.', 403);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
await next();
|
|
344
|
+
};
|
|
345
|
+
/** The CSRF token in the request body, if the body is a form at all. */
|
|
346
|
+
async function submittedToken(c) {
|
|
347
|
+
const contentType = c.req.header('content-type') ?? '';
|
|
348
|
+
if (!contentType.includes('application/x-www-form-urlencoded') &&
|
|
349
|
+
!contentType.includes('multipart/form-data')) {
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
// Hono caches the parsed form, so the handler behind this parses nothing twice.
|
|
353
|
+
const body = await c.req.parseBody();
|
|
354
|
+
return body[CSRF_FIELD];
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* A percent-decoded request path. A path that will not decode is handed on as
|
|
358
|
+
* it arrived, where it will match no file and become a 404.
|
|
359
|
+
*/
|
|
360
|
+
function decodePath(pathname) {
|
|
361
|
+
try {
|
|
362
|
+
return decodeURIComponent(pathname);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return pathname;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/** A form field as a string. A file upload, or a missing field, is the empty one. */
|
|
369
|
+
function text(value) {
|
|
370
|
+
return typeof value === 'string' ? value : '';
|
|
371
|
+
}
|
|
372
|
+
/** What is wrong with a proposed first admin, or `undefined` when nothing is. */
|
|
373
|
+
function setupProblem(username, password, confirmation) {
|
|
374
|
+
// The same rules `geekity user add` enforces, so an account made either way
|
|
375
|
+
// is an account the other door would have accepted.
|
|
376
|
+
const problem = credentialProblem(username, password);
|
|
377
|
+
if (problem !== undefined)
|
|
378
|
+
return problem;
|
|
379
|
+
if (password !== confirmation)
|
|
380
|
+
return 'The two passwords do not match.';
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/admin/routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { ResolvedConfig } from '../config.ts';
|
|
3
|
+
import type { GeekityEnv } from '../env.ts';
|
|
4
|
+
/** Where the admin lives. Everything under it needs a session. */
|
|
5
|
+
export declare const ADMIN_PREFIX = "/admin";
|
|
6
|
+
/** Name of the cookie holding the session id. */
|
|
7
|
+
export declare const SESSION_COOKIE = "geekity_session";
|
|
8
|
+
/** Name of the hidden field every mutating admin form carries. */
|
|
9
|
+
export declare const CSRF_FIELD = "csrf_token";
|
|
10
|
+
/**
|
|
11
|
+
* Whether the session cookie gets `Secure`, which is decided by the site's
|
|
12
|
+
* base URL rather than being hard-coded.
|
|
13
|
+
*
|
|
14
|
+
* doc-5 says the cookie is always `Secure`. It is not, quite: a `Secure`
|
|
15
|
+
* cookie is dropped on a plain-HTTP origin, so a site being developed on
|
|
16
|
+
* `http://localhost:3000` could never hold a login. `Secure` therefore goes on
|
|
17
|
+
* whenever the site says it is served over https, which covers every
|
|
18
|
+
* deployment, and comes off for local http, which is the only case it would
|
|
19
|
+
* have broken.
|
|
20
|
+
*/
|
|
21
|
+
export declare function usesSecureCookies(config: Pick<ResolvedConfig, 'baseUrl'>): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Put the session id in the response's cookie.
|
|
24
|
+
*
|
|
25
|
+
* `HttpOnly` keeps it away from scripts, `SameSite=Lax` keeps it off
|
|
26
|
+
* cross-site POSTs (belt to the CSRF token's braces), and `Path=/admin` keeps
|
|
27
|
+
* it off every public request, so a cached public page can never carry it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function setSessionCookie(c: Context<GeekityEnv>, sessionId: string, options: {
|
|
30
|
+
config: ResolvedConfig;
|
|
31
|
+
expiresAt: string;
|
|
32
|
+
}): void;
|
|
33
|
+
/** Drop the session cookie. The attributes have to match the ones it was set with. */
|
|
34
|
+
export declare function clearSessionCookie(c: Context<GeekityEnv>, config: ResolvedConfig): void;
|
|
35
|
+
/** The session id the request carries, or `undefined`. */
|
|
36
|
+
export declare function sessionIdFrom(c: Context<GeekityEnv>): string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Whether a submitted token is this session's, compared in constant time.
|
|
39
|
+
*
|
|
40
|
+
* Both are hex of a fixed length, so a length mismatch is already a mismatch
|
|
41
|
+
* and short-circuiting on it leaks nothing.
|
|
42
|
+
*/
|
|
43
|
+
export declare function csrfTokenMatches(expected: string, submitted: unknown): boolean;
|
|
44
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/admin/session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAGpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C,kEAAkE;AAClE,eAAO,MAAM,YAAY,WAAW,CAAC;AAErC,iDAAiD;AACjD,eAAO,MAAM,cAAc,oBAAoB,CAAC;AAEhD,kEAAkE;AAClE,eAAO,MAAM,UAAU,eAAe,CAAC;AAEvC;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,GAAG,OAAO,CAMlF;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,EACtB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACrD,IAAI,CAQN;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI,CAOvF;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,CAG9E"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { deleteCookie, getCookie, setCookie } from 'hono/cookie';
|
|
3
|
+
/** Where the admin lives. Everything under it needs a session. */
|
|
4
|
+
export const ADMIN_PREFIX = '/admin';
|
|
5
|
+
/** Name of the cookie holding the session id. */
|
|
6
|
+
export const SESSION_COOKIE = 'geekity_session';
|
|
7
|
+
/** Name of the hidden field every mutating admin form carries. */
|
|
8
|
+
export const CSRF_FIELD = 'csrf_token';
|
|
9
|
+
/**
|
|
10
|
+
* Whether the session cookie gets `Secure`, which is decided by the site's
|
|
11
|
+
* base URL rather than being hard-coded.
|
|
12
|
+
*
|
|
13
|
+
* doc-5 says the cookie is always `Secure`. It is not, quite: a `Secure`
|
|
14
|
+
* cookie is dropped on a plain-HTTP origin, so a site being developed on
|
|
15
|
+
* `http://localhost:3000` could never hold a login. `Secure` therefore goes on
|
|
16
|
+
* whenever the site says it is served over https, which covers every
|
|
17
|
+
* deployment, and comes off for local http, which is the only case it would
|
|
18
|
+
* have broken.
|
|
19
|
+
*/
|
|
20
|
+
export function usesSecureCookies(config) {
|
|
21
|
+
try {
|
|
22
|
+
return new URL(config.baseUrl).protocol === 'https:';
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Put the session id in the response's cookie.
|
|
30
|
+
*
|
|
31
|
+
* `HttpOnly` keeps it away from scripts, `SameSite=Lax` keeps it off
|
|
32
|
+
* cross-site POSTs (belt to the CSRF token's braces), and `Path=/admin` keeps
|
|
33
|
+
* it off every public request, so a cached public page can never carry it.
|
|
34
|
+
*/
|
|
35
|
+
export function setSessionCookie(c, sessionId, options) {
|
|
36
|
+
setCookie(c, SESSION_COOKIE, sessionId, {
|
|
37
|
+
path: ADMIN_PREFIX,
|
|
38
|
+
httpOnly: true,
|
|
39
|
+
sameSite: 'Lax',
|
|
40
|
+
secure: usesSecureCookies(options.config),
|
|
41
|
+
expires: new Date(options.expiresAt),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Drop the session cookie. The attributes have to match the ones it was set with. */
|
|
45
|
+
export function clearSessionCookie(c, config) {
|
|
46
|
+
deleteCookie(c, SESSION_COOKIE, {
|
|
47
|
+
path: ADMIN_PREFIX,
|
|
48
|
+
httpOnly: true,
|
|
49
|
+
sameSite: 'Lax',
|
|
50
|
+
secure: usesSecureCookies(config),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** The session id the request carries, or `undefined`. */
|
|
54
|
+
export function sessionIdFrom(c) {
|
|
55
|
+
const value = getCookie(c, SESSION_COOKIE);
|
|
56
|
+
return value === undefined || value === '' ? undefined : value;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Whether a submitted token is this session's, compared in constant time.
|
|
60
|
+
*
|
|
61
|
+
* Both are hex of a fixed length, so a length mismatch is already a mismatch
|
|
62
|
+
* and short-circuiting on it leaks nothing.
|
|
63
|
+
*/
|
|
64
|
+
export function csrfTokenMatches(expected, submitted) {
|
|
65
|
+
if (typeof submitted !== 'string' || submitted.length !== expected.length)
|
|
66
|
+
return false;
|
|
67
|
+
return timingSafeEqual(Buffer.from(submitted, 'utf8'), Buffer.from(expected, 'utf8'));
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/admin/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAKjE,kEAAkE;AAClE,MAAM,CAAC,MAAM,YAAY,GAAG,QAAQ,CAAC;AAErC,iDAAiD;AACjD,MAAM,CAAC,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAEhD,kEAAkE;AAClE,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAC;AAEvC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAuC;IACvE,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,CAAsB,EACtB,SAAiB,EACjB,OAAsD;IAEtD,SAAS,CAAC,CAAC,EAAE,cAAc,EAAE,SAAS,EAAE;QACtC,IAAI,EAAE,YAAY;QAClB,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,iBAAiB,CAAC,OAAO,CAAC,MAAM,CAAC;QACzC,OAAO,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC;KACrC,CAAC,CAAC;AACL,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,kBAAkB,CAAC,CAAsB,EAAE,MAAsB;IAC/E,YAAY,CAAC,CAAC,EAAE,cAAc,EAAE;QAC9B,IAAI,EAAE,YAAY;QAClB,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,iBAAiB,CAAC,MAAM,CAAC;KAClC,CAAC,CAAC;AACL,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,aAAa,CAAC,CAAsB;IAClD,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;IAC3C,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;AACjE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB,EAAE,SAAkB;IACnE,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxF,OAAO,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;AACxF,CAAC"}
|