@geekity/cms 0.8.0 → 0.9.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/README.md +292 -45
- package/admin/pages/comments/all.njk +3 -0
- package/admin/pages/settings/discussion.njk +53 -0
- package/admin/pages/settings/email.njk +20 -0
- package/admin/pages/tools/personal-data.njk +67 -0
- package/admin/pages/users/edit.njk +1 -1
- package/dist/admin/comments.d.ts +5 -0
- package/dist/admin/comments.d.ts.map +1 -1
- package/dist/admin/comments.js +13 -0
- package/dist/admin/comments.js.map +1 -1
- package/dist/admin/headers.d.ts +14 -5
- package/dist/admin/headers.d.ts.map +1 -1
- package/dist/admin/headers.js +18 -6
- package/dist/admin/headers.js.map +1 -1
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/menu.d.ts.map +1 -1
- package/dist/admin/menu.js +5 -1
- package/dist/admin/menu.js.map +1 -1
- package/dist/admin/personal-data.d.ts +35 -0
- package/dist/admin/personal-data.d.ts.map +1 -0
- package/dist/admin/personal-data.js +90 -0
- package/dist/admin/personal-data.js.map +1 -0
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +18 -2
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/session.d.ts +42 -1
- package/dist/admin/session.d.ts.map +1 -1
- package/dist/admin/session.js +74 -4
- package/dist/admin/session.js.map +1 -1
- package/dist/admin/settings-discussion.d.ts.map +1 -1
- package/dist/admin/settings-discussion.js +9 -1
- package/dist/admin/settings-discussion.js.map +1 -1
- package/dist/admin/settings-email.d.ts.map +1 -1
- package/dist/admin/settings-email.js +10 -1
- package/dist/admin/settings-email.js.map +1 -1
- package/dist/admin/settings.d.ts +39 -0
- package/dist/admin/settings.d.ts.map +1 -1
- package/dist/admin/settings.js +144 -0
- package/dist/admin/settings.js.map +1 -1
- package/dist/admin/store.d.ts +30 -0
- package/dist/admin/store.d.ts.map +1 -1
- package/dist/admin/store.js +68 -5
- package/dist/admin/store.js.map +1 -1
- package/dist/admin/templates.d.ts +1 -0
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +1 -0
- package/dist/admin/templates.js.map +1 -1
- package/dist/avatars/avatars.d.ts +112 -0
- package/dist/avatars/avatars.d.ts.map +1 -0
- package/dist/avatars/avatars.js +259 -0
- package/dist/avatars/avatars.js.map +1 -0
- package/dist/avatars/index.d.ts +4 -0
- package/dist/avatars/index.d.ts.map +1 -0
- package/dist/avatars/index.js +3 -0
- package/dist/avatars/index.js.map +1 -0
- package/dist/avatars/routes.d.ts +11 -0
- package/dist/avatars/routes.d.ts.map +1 -0
- package/dist/avatars/routes.js +33 -0
- package/dist/avatars/routes.js.map +1 -0
- package/dist/comments/records.d.ts +15 -0
- package/dist/comments/records.d.ts.map +1 -1
- package/dist/comments/records.js +51 -1
- package/dist/comments/records.js.map +1 -1
- package/dist/comments/routes.d.ts.map +1 -1
- package/dist/comments/routes.js +7 -1
- package/dist/comments/routes.js.map +1 -1
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +57 -0
- package/dist/config.js.map +1 -1
- package/dist/contact/index.d.ts +1 -1
- package/dist/contact/index.d.ts.map +1 -1
- package/dist/contact/index.js +1 -1
- package/dist/contact/index.js.map +1 -1
- package/dist/contact/records.d.ts +8 -2
- package/dist/contact/records.d.ts.map +1 -1
- package/dist/contact/records.js +13 -6
- package/dist/contact/records.js.map +1 -1
- package/dist/env.d.ts +6 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/federation/quotes.d.ts +2 -0
- package/dist/federation/quotes.d.ts.map +1 -1
- package/dist/federation/quotes.js +4 -0
- package/dist/federation/quotes.js.map +1 -1
- package/dist/federation/replies.d.ts +23 -0
- package/dist/federation/replies.d.ts.map +1 -1
- package/dist/federation/replies.js +47 -10
- package/dist/federation/replies.js.map +1 -1
- package/dist/index.d.ts +23 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +41 -7
- package/dist/index.js.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +1 -1
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/optouts.d.ts +6 -0
- package/dist/notifications/optouts.d.ts.map +1 -1
- package/dist/notifications/optouts.js +17 -0
- package/dist/notifications/optouts.js.map +1 -1
- package/dist/privacy/erase.d.ts +45 -0
- package/dist/privacy/erase.d.ts.map +1 -0
- package/dist/privacy/erase.js +77 -0
- package/dist/privacy/erase.js.map +1 -0
- package/dist/privacy/policy.d.ts +32 -0
- package/dist/privacy/policy.d.ts.map +1 -0
- package/dist/privacy/policy.js +37 -0
- package/dist/privacy/policy.js.map +1 -0
- package/dist/privacy/retention.d.ts +79 -0
- package/dist/privacy/retention.d.ts.map +1 -0
- package/dist/privacy/retention.js +133 -0
- package/dist/privacy/retention.js.map +1 -0
- package/dist/web/conversation.d.ts +9 -1
- package/dist/web/conversation.d.ts.map +1 -1
- package/dist/web/conversation.js +68 -7
- package/dist/web/conversation.js.map +1 -1
- package/dist/web/index.d.ts +1 -0
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +1 -0
- package/dist/web/index.js.map +1 -1
- package/dist/web/maintenance.d.ts.map +1 -1
- package/dist/web/maintenance.js +7 -2
- package/dist/web/maintenance.js.map +1 -1
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +4 -0
- package/dist/web/routes.js.map +1 -1
- package/dist/web/well-known.d.ts +21 -0
- package/dist/web/well-known.d.ts.map +1 -0
- package/dist/web/well-known.js +66 -0
- package/dist/web/well-known.js.map +1 -0
- package/dist/webmention/fetch-public.d.ts +34 -0
- package/dist/webmention/fetch-public.d.ts.map +1 -0
- package/dist/webmention/fetch-public.js +101 -0
- package/dist/webmention/fetch-public.js.map +1 -0
- package/dist/webmention/reply-context.d.ts.map +1 -1
- package/dist/webmention/reply-context.js +12 -89
- package/dist/webmention/reply-context.js.map +1 -1
- package/package.json +1 -1
- package/templates/site/content/_data/site.json +7 -1
- package/templates/site/content/pages/privacy.md +127 -0
- package/themes/default/README.md +38 -19
package/README.md
CHANGED
|
@@ -19,8 +19,8 @@ pnpm install
|
|
|
19
19
|
pnpm dev
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
That is a running site on <http://localhost:3000> with a post,
|
|
23
|
-
packaged theme. `geekity init` refuses a directory that already has anything in
|
|
22
|
+
That is a running site on <http://localhost:3000> with a post, an About page, a
|
|
23
|
+
starter privacy page and the packaged theme. `geekity init` refuses a directory that already has anything in
|
|
24
24
|
it, so it can never write over a site you already have.
|
|
25
25
|
|
|
26
26
|
What it writes:
|
|
@@ -32,7 +32,7 @@ server.ts the entry file; where your own routes go
|
|
|
32
32
|
tsconfig.json so the site type checks against the package
|
|
33
33
|
content/
|
|
34
34
|
posts/ Markdown posts, plus posts.json for Eleventy
|
|
35
|
-
pages/
|
|
35
|
+
pages/ about.md, privacy.md, plus pages.json
|
|
36
36
|
_data/site.json the settings: title, tagline, author, page and feed sizes
|
|
37
37
|
_data/federation/ once the site federates: followers.json and the inbox log
|
|
38
38
|
_data/comments/ once somebody comments: one JSON file per post
|
|
@@ -73,6 +73,13 @@ JSON representation and the content format are all covered by semver: a
|
|
|
73
73
|
breaking change to any of them is a major with a migration note in the
|
|
74
74
|
changelog.
|
|
75
75
|
|
|
76
|
+
Upgrading to the release that renamed the session cookie signs everybody out
|
|
77
|
+
of an https site once. See [Session hardening](#session-hardening).
|
|
78
|
+
|
|
79
|
+
Upgrading never deletes a reader's data. The release that added
|
|
80
|
+
[retention](#retention) keeps every commenter email, address hash and contact
|
|
81
|
+
message on a site whose `site.json` has not set a period.
|
|
82
|
+
|
|
76
83
|
## The `geekity` command
|
|
77
84
|
|
|
78
85
|
`geekity` is installed as a bin, so `pnpm geekity <command>` runs it inside a
|
|
@@ -276,6 +283,7 @@ directory; absolute ones are used as given.
|
|
|
276
283
|
| `accessLogAddress` | `false` | `GEEKITY_ACCESS_LOG_ADDRESS` | Put the client address on the end of each access-log line. `trustProxy` decides which address that is. |
|
|
277
284
|
| `accessLogWriter` | stdout | — | Where the lines go instead. See [The access log](#the-access-log). |
|
|
278
285
|
| `maintenance` | `false` | `GEEKITY_MAINTENANCE` | Keep the site in maintenance mode, answering 503, until a restart without it. |
|
|
286
|
+
| `securityHeaders` | [see below](#security-headers) | — | Headers every response carries. A string replaces a default or adds a header, `false` removes one. |
|
|
279
287
|
| `onDocumentChange` | none | — | Hook run for every change to the index. See [Hooks](#hooks). |
|
|
280
288
|
| `onPublish` | none | — | Hook run when a document becomes visible. See [Hooks](#hooks). |
|
|
281
289
|
| `federation` | `{}` | — | Federation stores and guards. See [Federation](#federation). |
|
|
@@ -363,13 +371,14 @@ the same directory reads all of it, and everything in it is meant to be public:
|
|
|
363
371
|
| `data/notification-digests.json` | When each user was last sent a digest. Mode `0600`. Losing it sends one digest early and nothing worse. |
|
|
364
372
|
| `data/wordpress-activitypub.json` | When each WordPress compatibility path was last asked for. Losing it resets the answer the switch is watched by. |
|
|
365
373
|
|
|
366
|
-
|
|
374
|
+
Three things under `data/` may be deleted whenever the site is stopped, and
|
|
367
375
|
nothing else in either directory may:
|
|
368
376
|
|
|
369
|
-
| Path | What it is
|
|
370
|
-
| ----------------- |
|
|
371
|
-
| `data/geekity.db` | The SQLite cache, with its `-wal` and `-shm`. `geekity rebuild` deletes and rebuilds it.
|
|
372
|
-
| `data/images/` | Variants derived from `content/uploads/`, with their `image.json` sidecars. `rm -r` it.
|
|
377
|
+
| Path | What it is |
|
|
378
|
+
| ----------------- | ----------------------------------------------------------------------------------------------- |
|
|
379
|
+
| `data/geekity.db` | The SQLite cache, with its `-wal` and `-shm`. `geekity rebuild` deletes and rebuilds it. |
|
|
380
|
+
| `data/images/` | Variants derived from `content/uploads/`, with their `image.json` sidecars. `rm -r` it. |
|
|
381
|
+
| `data/avatars/` | Remote avatars served from `/_geekity/avatars/` (TASK-134). `rm -r` it; they are fetched again. |
|
|
373
382
|
|
|
374
383
|
The next boot builds the database out of the files with no manual step, and a
|
|
375
384
|
request for a variant that is not there derives it and serves it.
|
|
@@ -1381,6 +1390,109 @@ failure than a spam list to glance at. A checker that is down or throwing is no
|
|
|
1381
1390
|
opinion, and the message is stored as it would have been before anybody had
|
|
1382
1391
|
one.
|
|
1383
1392
|
|
|
1393
|
+
## Personal data
|
|
1394
|
+
|
|
1395
|
+
This is every piece of personal data the CMS stores, where it is, and how long
|
|
1396
|
+
it stays. "Kept" means until somebody deletes it, unless a retention period
|
|
1397
|
+
says otherwise.
|
|
1398
|
+
|
|
1399
|
+
| What | Where | How long |
|
|
1400
|
+
| -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
1401
|
+
| A commenter's name, website and words | `content/_data/comments/{slug}.json`, public and in git | Kept. Erasing on request replaces the name and drops the website. |
|
|
1402
|
+
| A commenter's email, and whether they asked to be told about replies | `content/_data/comments/{slug}.json`, **never shown**, but in git | `commentEmailRetentionDays`: forever unless set, 180 on a new site. |
|
|
1403
|
+
| A salted hash of the address a comment, webmention or contact message came from (the address itself is never stored) | The comment file, or the contact message file | `addressHashRetentionDays`: forever unless set, 30 on a new site. |
|
|
1404
|
+
| A webmention's author name, website, avatar URL and the source page's words | `content/_data/comments/{slug}.json` | Kept, as a copy of a page that is public already. |
|
|
1405
|
+
| A contact message: the sender's name, email, subject and message | `data/contact/{id}.json`, mode `0600` | `contactMessageRetentionDays`: forever unless set, 365 on a new site. |
|
|
1406
|
+
| The addresses that unsubscribed from reply notices | `data/comment-optouts.json`, mode `0600` | Kept, so the site goes on not writing to them. Erasing on request removes one. |
|
|
1407
|
+
| Followers: actor id, handle, display name, avatar URL, profile URL | `content/_data/federation/{username}/followers.json`, in git | Until they unfollow. |
|
|
1408
|
+
| Inbound likes, boosts, replies and quotes, with the actor who sent them | `content/_data/federation/inbox/{yyyy}-{mm}.jsonl`, in git | Kept. |
|
|
1409
|
+
| What a reply shows of the post it answers: its title, words and author | `content/_data/replyContexts.json`, in git | Kept. |
|
|
1410
|
+
| Remote avatars, shrunk | `data/avatars/` | Deleted by the avatar sweep once nothing shown names them. |
|
|
1411
|
+
| Users: username, email, argon2id password hash, profile | `data/users.json`, mode `0600` | Until the user is deleted. |
|
|
1412
|
+
| An index of all of the above, and sessions, reset tokens and spent link tokens | `data/geekity.db` | A cache of the files. Sessions and tokens are pruned when they expire. |
|
|
1413
|
+
| Client addresses in the rate limits | Memory | Until the window passes or the site restarts. |
|
|
1414
|
+
| Client addresses in the access log | stdout, and whatever collects it | Only with `accessLogAddress` on. The collector keeps them. |
|
|
1415
|
+
|
|
1416
|
+
Two services outside the site see personal data when a site turns them on.
|
|
1417
|
+
Akismet is sent a commenter's or sender's address, user agent, referrer, name,
|
|
1418
|
+
email, website and words. The mail provider is sent every address a message
|
|
1419
|
+
goes to.
|
|
1420
|
+
|
|
1421
|
+
### The starter privacy page
|
|
1422
|
+
|
|
1423
|
+
`geekity init` writes `content/pages/privacy.md`, served at `/privacy/` and
|
|
1424
|
+
linked from the footer by a `Privacy | /privacy/` line in `menus.footer`. It
|
|
1425
|
+
says in plain language what the table above says, marks the contact form,
|
|
1426
|
+
reply emails and Akismet as optional, and quotes the retention periods a new
|
|
1427
|
+
site starts with. It is a starting point written from what the software does,
|
|
1428
|
+
not legal advice, and a note at its top says so to the owner: review it, add
|
|
1429
|
+
whatever the site does beyond the CMS (analytics, a host's logs, a theme that
|
|
1430
|
+
loads fonts from elsewhere), change the numbers when a retention period
|
|
1431
|
+
changes, and remove the menu line if the page is deleted.
|
|
1432
|
+
|
|
1433
|
+
The link is a menu line rather than something the theme prints, because the
|
|
1434
|
+
footer prints `menus.footer` and nothing of its own (TASK-105). A site that
|
|
1435
|
+
upgrades gets no page and no link; it can copy the file from
|
|
1436
|
+
`templates/site/content/pages/privacy.md` in the package.
|
|
1437
|
+
|
|
1438
|
+
### Retention
|
|
1439
|
+
|
|
1440
|
+
Three settings in `content/_data/site.json`, on **Settings > Discussion**,
|
|
1441
|
+
decide how long the site keeps what readers hand it. Each counts whole days from
|
|
1442
|
+
when the data arrived, and `0` keeps it forever:
|
|
1443
|
+
|
|
1444
|
+
| Setting | Recommended | What goes when it runs out |
|
|
1445
|
+
| ----------------------------- | ----------- | ----------------------------------------------------------------- |
|
|
1446
|
+
| `commentEmailRetentionDays` | `180` | The email on a comment, and with it the reply subscription. |
|
|
1447
|
+
| `addressHashRetentionDays` | `30` | The address hash on a comment, a webmention or a contact message. |
|
|
1448
|
+
| `contactMessageRetentionDays` | `365` | The whole contact message. |
|
|
1449
|
+
|
|
1450
|
+
**A site keeps everything until its owner sets a period.** A `site.json`
|
|
1451
|
+
without these keys reads as `0` for all three, so an existing site that
|
|
1452
|
+
upgrades loses nothing, and the Discussion screen says "Kept forever" beside
|
|
1453
|
+
each one. A new site from `geekity init` starts with the recommended periods
|
|
1454
|
+
written into its `site.json`. An existing site opts in by typing them on
|
|
1455
|
+
Settings > Discussion and saving once.
|
|
1456
|
+
|
|
1457
|
+
They are settings rather than config because how long a site keeps people's
|
|
1458
|
+
data is the owner's decision, and a privacy notice quotes it; `site.json` is
|
|
1459
|
+
public, which suits a policy. The recommended periods are long enough for a
|
|
1460
|
+
reply notice to arrive, a run of spam to be spotted and a year of
|
|
1461
|
+
correspondence to be answered, and no longer. The first sweep after a site
|
|
1462
|
+
sets a period removes whatever is already past it.
|
|
1463
|
+
|
|
1464
|
+
A sweep runs when the site starts serving and every six hours after that, and
|
|
1465
|
+
`cms.retention.sweep()` runs one now. It reads the files rather than the index,
|
|
1466
|
+
rewrites each comment file and message inside the same lock every other writer
|
|
1467
|
+
takes, and puts each changed comment back into the index in the same step. A
|
|
1468
|
+
second sweep finds nothing left to remove and writes nothing.
|
|
1469
|
+
|
|
1470
|
+
A comment that loses its email keeps its id, its place in the thread, its
|
|
1471
|
+
status, its words and its name. It gains `"redacted": ["email"]`, which is how
|
|
1472
|
+
the index tells an email removed from one never given: a name whose email was
|
|
1473
|
+
removed does not count toward auto-approval, so nobody can type that name and
|
|
1474
|
+
skip the queue. Its author is no longer told about replies, and their next
|
|
1475
|
+
comment waits for a moderator. A moderation link in an inbox names the comment
|
|
1476
|
+
by id, so it goes on working.
|
|
1477
|
+
|
|
1478
|
+
The sweep changes the files as they are now. A comment file in git keeps its
|
|
1479
|
+
history, so an email committed before it was removed is still in the
|
|
1480
|
+
repository's history until that history is rewritten.
|
|
1481
|
+
|
|
1482
|
+
### Erasing one person's data
|
|
1483
|
+
|
|
1484
|
+
**Tools > Personal data** erases what the site holds for one email address.
|
|
1485
|
+
It shows what it found first: how many comments carry the address, how many
|
|
1486
|
+
contact messages came from it, and whether it is on the opt-out list. Erasing
|
|
1487
|
+
then signs each of those comments `Anonymous`, removes its email, website,
|
|
1488
|
+
avatar and address hash, and marks it `"redacted": ["email", "addressHash",
|
|
1489
|
+
"author"]`; the comment keeps its place in the thread, its status and its words.
|
|
1490
|
+
The contact messages are deleted and the address comes off the opt-out list.
|
|
1491
|
+
|
|
1492
|
+
A comment is found by its email, so one whose email the sweep has already
|
|
1493
|
+
removed, a webmention and a fediverse reply are not found there. The Comments
|
|
1494
|
+
screen deletes those one at a time.
|
|
1495
|
+
|
|
1384
1496
|
## Keeping the index in step
|
|
1385
1497
|
|
|
1386
1498
|
Booting scans `contentDir`, indexes every Markdown file under `posts/` and
|
|
@@ -1585,7 +1697,7 @@ shadow the login form.
|
|
|
1585
1697
|
| `/admin/federation/resend` | `POST` only. Sends one post to the followers again, as its file now reads. |
|
|
1586
1698
|
| `/admin/setup` | First run: creates the first admin. Closed once a user exists. |
|
|
1587
1699
|
| `/admin/login` | Username and password. |
|
|
1588
|
-
| `/admin/logout` | `POST` only. Deletes the session row
|
|
1700
|
+
| `/admin/logout` | `POST` only. Deletes the session row and sends `Clear-Site-Data`. |
|
|
1589
1701
|
| `/admin/_static/*` | The admin's own stylesheet and scripts, cached for an hour. |
|
|
1590
1702
|
|
|
1591
1703
|
The screens behind the login share one layout: a bar across the top with the
|
|
@@ -1642,11 +1754,13 @@ Passwords are hashed with argon2id through `node:crypto`, so there is no native
|
|
|
1642
1754
|
module to build. The cost parameters travel with each hash, which means raising
|
|
1643
1755
|
them later leaves every password already stored verifiable.
|
|
1644
1756
|
|
|
1645
|
-
A session id is 256 random bits in a `HttpOnly; SameSite=Lax; Path
|
|
1646
|
-
|
|
1647
|
-
`
|
|
1648
|
-
|
|
1649
|
-
|
|
1757
|
+
A session id is 256 random bits in a `HttpOnly; SameSite=Lax; Path=/` cookie.
|
|
1758
|
+
When `baseUrl` is an `https` URL the cookie is `Secure` and named
|
|
1759
|
+
`__Host-geekity_session`; over plain http it is `geekity_session`, as
|
|
1760
|
+
[Session hardening](#session-hardening) explains. It lasts `sessionLifetime`;
|
|
1761
|
+
an expired session is deleted rather than merely ignored. Every mutating admin
|
|
1762
|
+
form carries a per-session CSRF token, and a `POST` without a valid one is
|
|
1763
|
+
refused with 403 — including the login and setup forms,
|
|
1650
1764
|
which get the token from a short anonymous session created when the form is
|
|
1651
1765
|
first rendered. Logging in throws that session away and starts a new one, so a
|
|
1652
1766
|
planted session id cannot become a logged-in one.
|
|
@@ -1716,7 +1830,8 @@ The file carries `title`, `tagline`, `url`, `author`, `postsPerPage`,
|
|
|
1716
1830
|
`timezone`, `language`, `tagBase`,
|
|
1717
1831
|
`categoryBase`, `notifyServer`, `webmentionsSend`, `webmentionsReceive`,
|
|
1718
1832
|
`mailProvider`, `mailFromName`, `mailFromAddress`, `mailReplyTo`,
|
|
1719
|
-
`contactEmail`, `
|
|
1833
|
+
`contactEmail`, `securityContacts`, `securityPolicy`, `securityLanguages`,
|
|
1834
|
+
`relays`, `menus` and `taxonomyRedirects`,
|
|
1720
1835
|
and every other key it already had is kept, `feedSize` and anything a site put
|
|
1721
1836
|
there included. A key it does not carry is the default, and a key of the wrong
|
|
1722
1837
|
type is the default too: a hand-edited `site.json` cannot take the site down.
|
|
@@ -1950,6 +2065,49 @@ header; the leftmost entry is then used. With neither available, which is what
|
|
|
1950
2065
|
happens when the app is driven in process rather than served, the username is
|
|
1951
2066
|
the only key.
|
|
1952
2067
|
|
|
2068
|
+
### Session hardening
|
|
2069
|
+
|
|
2070
|
+
Three things protect a signed-in session beyond the cookie flags and the CSRF
|
|
2071
|
+
token.
|
|
2072
|
+
|
|
2073
|
+
**The cookie is `__Host-geekity_session` under https.** A browser accepts a
|
|
2074
|
+
cookie with the `__Host-` prefix only when it is `Secure`, has `Path=/` and
|
|
2075
|
+
names no `Domain`. That binds it to the exact host, so a page on a sibling
|
|
2076
|
+
subdomain cannot set or overwrite it to plant a session. The prefix needs
|
|
2077
|
+
`Secure`, and a browser drops a `Secure` cookie on a plain-http origin, so a
|
|
2078
|
+
site whose `baseUrl` is `http://localhost:3000` keeps the name
|
|
2079
|
+
`geekity_session` and local development works as before.
|
|
2080
|
+
|
|
2081
|
+
**The upgrade signs people out once.** A session made before this change is in
|
|
2082
|
+
a cookie named `geekity_session`. Under https that name is never accepted as a
|
|
2083
|
+
login again, because accepting it would give a sibling subdomain the way in
|
|
2084
|
+
that the prefix closes. The first admin request that carries only the old
|
|
2085
|
+
cookie deletes its session, expires the old cookie, and sends the browser to
|
|
2086
|
+
the login form with a message that the sign-in cookie was renamed and asks the
|
|
2087
|
+
person to sign in again. On the public site the old cookie is ignored, so a
|
|
2088
|
+
signed-in commenter sees the stranger's form until they sign in again.
|
|
2089
|
+
|
|
2090
|
+
**Fetch Metadata refuses cross-site writes.** A browser sends
|
|
2091
|
+
`Sec-Fetch-Site` with every request, and a page cannot change it. Every
|
|
2092
|
+
request under `/admin` other than `GET`, `HEAD` and `OPTIONS`, including the
|
|
2093
|
+
login and setup forms, is refused with 403 when that header says `cross-site`
|
|
2094
|
+
or `same-site`, before the session is read or a handler runs. A comment posted
|
|
2095
|
+
by somebody signed in gets the same check. `same-origin` and `none` pass. A
|
|
2096
|
+
request with no `Sec-Fetch-Site`, from an older browser or a script, falls back
|
|
2097
|
+
to the CSRF token, which every such request still has to carry. Nothing else
|
|
2098
|
+
is checked: the ActivityPub inboxes, the WordPress-compatible inbox,
|
|
2099
|
+
webmentions, the contact form, a stranger's comment, and the one-click
|
|
2100
|
+
moderate and unsubscribe links in an email take cross-site `POST`s by design,
|
|
2101
|
+
and they act on no session.
|
|
2102
|
+
|
|
2103
|
+
**Logout clears the site's data in the browser.** The logout response sends
|
|
2104
|
+
`Clear-Site-Data: "cache", "cookies", "storage"`, so a shared computer does not
|
|
2105
|
+
keep admin pages in its cache or anything the site stored. Browsers honour it
|
|
2106
|
+
only on a secure origin, which includes `http://localhost`. The `cookies`
|
|
2107
|
+
directive clears cookies for the whole registrable domain, so signing out of
|
|
2108
|
+
`blog.example.com` also signs the browser out of other sites under
|
|
2109
|
+
`example.com`.
|
|
2110
|
+
|
|
1953
2111
|
### Forgotten passwords
|
|
1954
2112
|
|
|
1955
2113
|
`/admin/login` carries a **Forgotten your password?** link to `/admin/forgot`,
|
|
@@ -1993,12 +2151,67 @@ no restart.
|
|
|
1993
2151
|
Every response the CMS sends carries `X-Content-Type-Options: nosniff`, and
|
|
1994
2152
|
`Strict-Transport-Security: max-age=31536000; includeSubDomains` when `baseUrl`
|
|
1995
2153
|
is an `https` URL — the same test that decides whether the session cookie is
|
|
1996
|
-
`Secure`, so the two cannot disagree.
|
|
1997
|
-
|
|
1998
|
-
|
|
2154
|
+
`Secure`, so the two cannot disagree. Neither can be turned off.
|
|
2155
|
+
|
|
2156
|
+
Every response also carries these, public pages, feeds, uploads, ActivityPub
|
|
2157
|
+
JSON, redirects, 404s, the maintenance 503 and the 500 page alike:
|
|
2158
|
+
|
|
2159
|
+
| Header | Default |
|
|
2160
|
+
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2161
|
+
| `Referrer-Policy` | `strict-origin-when-cross-origin` |
|
|
2162
|
+
| `Content-Security-Policy` | `frame-ancestors 'self'` |
|
|
2163
|
+
| `X-Frame-Options` | `SAMEORIGIN` |
|
|
2164
|
+
| `Permissions-Policy` | `browsing-topics=(), camera=(), display-capture=(), geolocation=(), hid=(), microphone=(), midi=(), payment=(), serial=(), usb=(), xr-spatial-tracking=()` |
|
|
2165
|
+
| `Cross-Origin-Opener-Policy` | `same-origin` |
|
|
2166
|
+
|
|
2167
|
+
None of them limits what a page may load, and that is the line the CMS holds
|
|
2168
|
+
on the public site. There is no content policy, no `default-src`, `script-src`
|
|
2169
|
+
or `img-src`, because a theme is somebody else's HTML and the CMS has no
|
|
2170
|
+
business deciding what it may reference. `frame-ancestors` is the only
|
|
2171
|
+
directive in the policy, and it is about who may put the page in a frame, not
|
|
2172
|
+
what is in it. Each header does something else:
|
|
2173
|
+
|
|
2174
|
+
- `Referrer-Policy` sends only the origin, not the full URL, when a reader
|
|
2175
|
+
follows a link to another site, so the path and query of the page they were
|
|
2176
|
+
on stay on the site.
|
|
2177
|
+
- `frame-ancestors 'self'` and `X-Frame-Options: SAMEORIGIN` stop another site
|
|
2178
|
+
from framing a page. That matters most for a signed-in user, whose pages carry
|
|
2179
|
+
a comment form that posts as them, but it covers every page because the CMS
|
|
2180
|
+
cannot tell a framed reader from a framed user.
|
|
2181
|
+
- `Permissions-Policy` turns off browser features a blog has no use for. It
|
|
2182
|
+
leaves alone what an embedded video player asks for: autoplay, fullscreen,
|
|
2183
|
+
`encrypted-media`, picture-in-picture, and the motion sensors behind a
|
|
2184
|
+
360-degree video.
|
|
2185
|
+
- `Cross-Origin-Opener-Policy: same-origin` keeps a window the site opens, or a
|
|
2186
|
+
window that opens the site, from reaching into it through `window.opener`.
|
|
2187
|
+
|
|
2188
|
+
Two of them can get in the way of something a site does on purpose. A site that
|
|
2189
|
+
is meant to be framed elsewhere, such as in a portfolio or a slide deck, has to
|
|
2190
|
+
name that host in `frame-ancestors` and remove `X-Frame-Options`, which cannot
|
|
2191
|
+
name another host. A theme that signs readers in or takes payment through a
|
|
2192
|
+
popup on another origin needs `same-origin-allow-popups` for COOP. The CMS
|
|
2193
|
+
itself does neither.
|
|
2194
|
+
|
|
2195
|
+
A site changes them with `securityHeaders` in its config. Names are matched
|
|
2196
|
+
without regard to case. A string replaces a default or adds a header of the
|
|
2197
|
+
site's own, and `false` removes a default:
|
|
2198
|
+
|
|
2199
|
+
```ts
|
|
2200
|
+
export default defineConfig({
|
|
2201
|
+
securityHeaders: {
|
|
2202
|
+
'Content-Security-Policy':
|
|
2203
|
+
"frame-ancestors 'self' https://portfolio.example",
|
|
2204
|
+
'X-Frame-Options': false,
|
|
2205
|
+
'Cross-Origin-Opener-Policy': 'same-origin-allow-popups',
|
|
2206
|
+
},
|
|
2207
|
+
});
|
|
2208
|
+
```
|
|
2209
|
+
|
|
2210
|
+
A name that is not a header name, or a value that is empty or has a line break
|
|
2211
|
+
in it, stops the site at boot. There is no environment variable for these.
|
|
1999
2212
|
|
|
2000
|
-
Every response under `/admin`, static files and redirects included,
|
|
2001
|
-
|
|
2213
|
+
Every response under `/admin`, static files and redirects included, gets three
|
|
2214
|
+
stricter values in their place, whatever `securityHeaders` says:
|
|
2002
2215
|
|
|
2003
2216
|
| Header | Value |
|
|
2004
2217
|
| ------------------------- | --------------------------------------------- |
|
|
@@ -2027,35 +2240,69 @@ uploads. Four directives say more than that:
|
|
|
2027
2240
|
sandboxed `srcdoc` iframe, which inherits this policy, and its one ancestor is
|
|
2028
2241
|
the admin page itself.
|
|
2029
2242
|
|
|
2243
|
+
### security.txt and change-password
|
|
2244
|
+
|
|
2245
|
+
`/.well-known/security.txt` ([RFC 9116](https://www.rfc-editor.org/rfc/rfc9116))
|
|
2246
|
+
tells somebody who finds a vulnerability in the site where to report it. It is
|
|
2247
|
+
built from three settings on the Email settings page, under Security contact,
|
|
2248
|
+
which `content/_data/site.json` holds:
|
|
2249
|
+
|
|
2250
|
+
| Setting | `site.json` key | Default | What it becomes |
|
|
2251
|
+
| ------------------- | ------------------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
2252
|
+
| Contacts | `securityContacts` | `[]` | One `Contact:` line each, in order. An email address, stored as `mailto:…`, an `https://` URL or a `tel:` number. |
|
|
2253
|
+
| Disclosure policy | `securityPolicy` | `""` | A `Policy:` line. An `https://` URL, or empty for no line. |
|
|
2254
|
+
| Preferred languages | `securityLanguages` | `""` | A `Preferred-Languages:` line. Language tags separated by commas, or empty for no line. |
|
|
2255
|
+
|
|
2256
|
+
With no contact the path is a 404: the RFC requires at least one. The
|
|
2257
|
+
`contactEmail` setting is never used in its place, because that address is
|
|
2258
|
+
promised never to appear on the public site.
|
|
2259
|
+
|
|
2260
|
+
The file is built on every request and served as `text/plain; charset=utf-8`.
|
|
2261
|
+
`Expires` is 30 days after the request, so it never goes stale however long the
|
|
2262
|
+
site runs, and `Canonical` is the path under the base URL in effect. There is no
|
|
2263
|
+
config-file key or environment variable for any of these: like `contactEmail`,
|
|
2264
|
+
they are site settings a person edits on the settings screen.
|
|
2265
|
+
|
|
2266
|
+
`/.well-known/change-password` is where a password manager sends somebody who
|
|
2267
|
+
wants to change a saved password. It redirects with a `302` and
|
|
2268
|
+
`Cache-Control: no-store`: to `/admin/users/{id}#change-password`, the form on
|
|
2269
|
+
the signed-in user's own page, or to the login form when nobody is signed in.
|
|
2270
|
+
|
|
2271
|
+
Both paths answer as usual in maintenance mode. A researcher may need the
|
|
2272
|
+
security contact most while the site is down, and change-password only points
|
|
2273
|
+
into the admin, which maintenance mode leaves open.
|
|
2274
|
+
|
|
2030
2275
|
## The public site
|
|
2031
2276
|
|
|
2032
2277
|
Booting mounts the public site on the app. The routes are:
|
|
2033
2278
|
|
|
2034
|
-
| Route | What it serves
|
|
2035
|
-
| --------------------------------------- |
|
|
2036
|
-
| `/` | Published posts, newest first.
|
|
2037
|
-
| `/page/2/` and up | Later pages of the same archive.
|
|
2038
|
-
| a document's permalink | The post or the page, through the theme.
|
|
2039
|
-
| `/tag/{tag}/` | Everything published carrying that tag, paginated at `/tag/{tag}/page/2/`.
|
|
2040
|
-
| `/category/{name}/` | The second taxonomy, paginated the same way.
|
|
2041
|
-
| `/author/{username}/` | One user's published posts, headed by their profile, paginated the same way.
|
|
2042
|
-
| `/feed/`, `/feed/atom/`, `/feed/json/` | The recent posts as RSS 2.0, Atom and JSON Feed.
|
|
2043
|
-
| `/tag/{tag}/feed/` and its two siblings | The same, for one tag; `/category/{name}/feed/` likewise.
|
|
2044
|
-
| `/author/{username}/feed/` and siblings | The same, for one person.
|
|
2045
|
-
| `/comments/feed/` | Every reply the inbox has been sent, as RSS 2.0.
|
|
2046
|
-
| `{permalink}feed/` | One post's replies, the same way.
|
|
2047
|
-
| `/sitemap.xml` | Every public URL, for a search engine.
|
|
2048
|
-
| `/sitemap-{n}.xml` | One file of a sitemap too big to be a single one.
|
|
2049
|
-
| `/robots.txt` | What a crawler may have, and where the sitemap is.
|
|
2050
|
-
|
|
|
2051
|
-
|
|
|
2052
|
-
| `/_geekity/
|
|
2053
|
-
| `/_geekity/
|
|
2054
|
-
| `/_geekity/
|
|
2055
|
-
| `/
|
|
2056
|
-
| `/
|
|
2057
|
-
| `/
|
|
2058
|
-
|
|
|
2279
|
+
| Route | What it serves |
|
|
2280
|
+
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
2281
|
+
| `/` | Published posts, newest first. |
|
|
2282
|
+
| `/page/2/` and up | Later pages of the same archive. |
|
|
2283
|
+
| a document's permalink | The post or the page, through the theme. |
|
|
2284
|
+
| `/tag/{tag}/` | Everything published carrying that tag, paginated at `/tag/{tag}/page/2/`. |
|
|
2285
|
+
| `/category/{name}/` | The second taxonomy, paginated the same way. |
|
|
2286
|
+
| `/author/{username}/` | One user's published posts, headed by their profile, paginated the same way. |
|
|
2287
|
+
| `/feed/`, `/feed/atom/`, `/feed/json/` | The recent posts as RSS 2.0, Atom and JSON Feed. |
|
|
2288
|
+
| `/tag/{tag}/feed/` and its two siblings | The same, for one tag; `/category/{name}/feed/` likewise. |
|
|
2289
|
+
| `/author/{username}/feed/` and siblings | The same, for one person. |
|
|
2290
|
+
| `/comments/feed/` | Every reply the inbox has been sent, as RSS 2.0. |
|
|
2291
|
+
| `{permalink}feed/` | One post's replies, the same way. |
|
|
2292
|
+
| `/sitemap.xml` | Every public URL, for a search engine. |
|
|
2293
|
+
| `/sitemap-{n}.xml` | One file of a sitemap too big to be a single one. |
|
|
2294
|
+
| `/robots.txt` | What a crawler may have, and where the sitemap is. |
|
|
2295
|
+
| `/.well-known/security.txt` | Where to report a vulnerability; a 404 until a security contact is set. See [above](#securitytxt-and-change-password). |
|
|
2296
|
+
| `/.well-known/change-password` | A redirect to the signed-in user's change-password form, or to the login form. |
|
|
2297
|
+
| `/_geekity/comments` | `POST` only. Where the comment form under a post submits. |
|
|
2298
|
+
| `/_geekity/contact` | `POST` only. Where the contact form on a page submits. |
|
|
2299
|
+
| `/_geekity/webmention` | `POST` only. Where a webmention is sent; advertised on every document. |
|
|
2300
|
+
| `/_geekity/moderate` | Where an approve, spam or delete link from a notification lands. `GET` shows a button; `POST` acts. |
|
|
2301
|
+
| `/_geekity/unsubscribe` | Where the unsubscribe link in a reply notice lands. Same two steps. |
|
|
2302
|
+
| `/theme/…` | The theme's own files, from its `static/` directory. |
|
|
2303
|
+
| `/uploads/…` | A file from `content/uploads/`, byte for byte as it was stored. |
|
|
2304
|
+
| `/uploads/_/…` | One derived copy of an uploaded image, generated on the spot if missing. |
|
|
2305
|
+
| anything else | The theme's 404. |
|
|
2059
2306
|
|
|
2060
2307
|
The federation routes go on before it, and answer only their own paths:
|
|
2061
2308
|
|
|
@@ -60,6 +60,9 @@
|
|
|
60
60
|
{% if row.address %}
|
|
61
61
|
· <code title="A salted hash of the address it came from">{{ row.address }}</code>
|
|
62
62
|
{% endif %}
|
|
63
|
+
{% if row.removed %}
|
|
64
|
+
· {{ row.removed }}
|
|
65
|
+
{% endif %}
|
|
63
66
|
</p>
|
|
64
67
|
|
|
65
68
|
<div class="admin-comment-body">{{ row.html | safe }}</div>
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
{% extends "layouts/settings-page.njk" %}
|
|
3
3
|
{% import "components/fields.njk" as field %}
|
|
4
4
|
|
|
5
|
+
{# What a retention field says about the period as it stands: kept forever at
|
|
6
|
+
0, removed after so many days otherwise. Nothing for a value the form is
|
|
7
|
+
about to refuse, which has its own error line. #}
|
|
8
|
+
{% macro retention(days) %}
|
|
9
|
+
{%- if days == '0' -%}
|
|
10
|
+
<strong>Kept forever.</strong>
|
|
11
|
+
{%- elif days | int > 0 -%}
|
|
12
|
+
<strong>Removed after {{ days }} day{% if days != '1' %}s{% endif %}.</strong>
|
|
13
|
+
{%- endif -%}
|
|
14
|
+
{% endmacro %}
|
|
15
|
+
|
|
5
16
|
{% block settingsFields %}
|
|
6
17
|
<h2>Comments</h2>
|
|
7
18
|
|
|
@@ -41,6 +52,48 @@
|
|
|
41
52
|
{% endset %}
|
|
42
53
|
{{ field.checkbox('settings-webmentions-receive', fields.webmentionsReceive,
|
|
43
54
|
'Accept webmentions sent here', checked=form.webmentionsReceive, hint=receiveHint) }}
|
|
55
|
+
|
|
56
|
+
<h2>Keeping personal data</h2>
|
|
57
|
+
|
|
58
|
+
<p class="admin-hint">
|
|
59
|
+
How long the site keeps what commenters and people writing through the
|
|
60
|
+
contact form hand it, in days from when it arrived; <code>0</code> keeps
|
|
61
|
+
it forever. A site that has never set these keeps everything, so upgrading
|
|
62
|
+
removes nothing. The recommended periods are 180 days for emails, 30 for
|
|
63
|
+
address hashes and 365 for contact messages, which is what a new site
|
|
64
|
+
starts with. A sweep removes whatever has outlived its period every few
|
|
65
|
+
hours. Tools › Personal data erases one person's data on request.
|
|
66
|
+
</p>
|
|
67
|
+
|
|
68
|
+
{% set emailHint %}
|
|
69
|
+
{{ retention(form.commentEmailRetentionDays) }}
|
|
70
|
+
Past this a comment loses its author's email. The comment, its place in
|
|
71
|
+
the thread and its moderation status stay; the author is no longer told
|
|
72
|
+
about replies, and their next comment waits for a moderator. Recommended:
|
|
73
|
+
<code>180</code>.
|
|
74
|
+
{% endset %}
|
|
75
|
+
{{ field.text('settings-comment-email-retention', fields.commentEmailRetentionDays,
|
|
76
|
+
'Keep commenter emails for', form.commentEmailRetentionDays, type='number', min='0',
|
|
77
|
+
step='1', error=problems.commentEmailRetentionDays, hint=emailHint) }}
|
|
78
|
+
|
|
79
|
+
{% set hashHint %}
|
|
80
|
+
{{ retention(form.addressHashRetentionDays) }}
|
|
81
|
+
The salted hash of the address a comment, webmention or contact message
|
|
82
|
+
came from, which shows a moderator that two came from one place.
|
|
83
|
+
Recommended: <code>30</code>.
|
|
84
|
+
{% endset %}
|
|
85
|
+
{{ field.text('settings-address-hash-retention', fields.addressHashRetentionDays,
|
|
86
|
+
'Keep address hashes for', form.addressHashRetentionDays, type='number', min='0',
|
|
87
|
+
step='1', error=problems.addressHashRetentionDays, hint=hashHint) }}
|
|
88
|
+
|
|
89
|
+
{% set contactHint %}
|
|
90
|
+
{{ retention(form.contactMessageRetentionDays) }}
|
|
91
|
+
Past this a contact message is deleted from the Messages screen and from
|
|
92
|
+
<code>data/contact/</code>. Recommended: <code>365</code>.
|
|
93
|
+
{% endset %}
|
|
94
|
+
{{ field.text('settings-contact-message-retention', fields.contactMessageRetentionDays,
|
|
95
|
+
'Keep contact messages for', form.contactMessageRetentionDays, type='number', min='0',
|
|
96
|
+
step='1', error=problems.contactMessageRetentionDays, hint=contactHint) }}
|
|
44
97
|
{% endblock %}
|
|
45
98
|
|
|
46
99
|
{% block settingsPanels %}
|
|
@@ -39,6 +39,26 @@
|
|
|
39
39
|
{% endset %}
|
|
40
40
|
{{ field.text('settings-contact-email', fields.contactEmail, 'Contact address',
|
|
41
41
|
form.contactEmail, error=problems.contactEmail, hint=contactHint, inputmode='email') }}
|
|
42
|
+
|
|
43
|
+
<h2>Security contact</h2>
|
|
44
|
+
|
|
45
|
+
{% set securityContactsHint %}
|
|
46
|
+
Where somebody who finds a vulnerability in this site should report it, one per line:
|
|
47
|
+
an email address, an <code>https://</code> URL or a <code>tel:</code> number. They are
|
|
48
|
+
published at <a href="/.well-known/security.txt"><code>/.well-known/security.txt</code></a>,
|
|
49
|
+
which is not served at all while this is empty.
|
|
50
|
+
{% endset %}
|
|
51
|
+
{{ field.textarea('settings-security-contacts', fields.securityContacts, 'Contacts',
|
|
52
|
+
form.securityContacts, error=problems.securityContacts, hint=securityContactsHint,
|
|
53
|
+
rows='2', spellcheck='false') }}
|
|
54
|
+
|
|
55
|
+
{{ field.text('settings-security-policy', fields.securityPolicy, 'Disclosure policy',
|
|
56
|
+
form.securityPolicy, error=problems.securityPolicy, inputmode='url',
|
|
57
|
+
hint='Optional. An https:// URL for the page that says how reports are handled.') }}
|
|
58
|
+
|
|
59
|
+
{{ field.text('settings-security-languages', fields.securityLanguages, 'Preferred languages',
|
|
60
|
+
form.securityLanguages, error=problems.securityLanguages,
|
|
61
|
+
hint='Optional. The languages a report may be written in, such as en, fr.') }}
|
|
42
62
|
{% endblock %}
|
|
43
63
|
|
|
44
64
|
{% block settingsPanels %}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{# /admin/tools/personal-data: erasing one person's data when they ask
|
|
2
|
+
(TASK-135). Looking is a step of its own because erasing cannot be undone. #}
|
|
3
|
+
{% extends "layouts/shell.njk" %}
|
|
4
|
+
{% import "components/fields.njk" as field %}
|
|
5
|
+
|
|
6
|
+
{% block title %}{{ heading }}{% endblock %}
|
|
7
|
+
|
|
8
|
+
{% block content %}
|
|
9
|
+
<h1>{{ heading }}</h1>
|
|
10
|
+
|
|
11
|
+
<p>
|
|
12
|
+
When somebody asks for what this site holds about them to be erased, type
|
|
13
|
+
the email address they commented or wrote in under. Their comments stay in
|
|
14
|
+
their threads, with their words and their moderation status, signed
|
|
15
|
+
<strong>{{ erasedName }}</strong> and with their email, website and address
|
|
16
|
+
hash removed. Their contact messages are deleted, and the address comes off
|
|
17
|
+
the list of people who asked not to be emailed about replies.
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
{% if found %}
|
|
21
|
+
<form class="admin-settings" method="post" action="{{ actionUrl }}">
|
|
22
|
+
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
23
|
+
<input type="hidden" name="{{ fields.email }}" value="{{ email }}" />
|
|
24
|
+
<input type="hidden" name="{{ fields.confirm }}" value="1" />
|
|
25
|
+
|
|
26
|
+
<h2>What this site holds for {{ email }}</h2>
|
|
27
|
+
|
|
28
|
+
<ul>
|
|
29
|
+
<li>{{ found.comments }}</li>
|
|
30
|
+
<li>{{ found.messages }}</li>
|
|
31
|
+
<li>
|
|
32
|
+
{% if found.optedOut %}On{% else %}Not on{% endif %} the list of addresses not
|
|
33
|
+
to email about replies
|
|
34
|
+
</li>
|
|
35
|
+
</ul>
|
|
36
|
+
|
|
37
|
+
{% if found.anything %}
|
|
38
|
+
<p>
|
|
39
|
+
Erasing cannot be undone. A copy of a comment file in git history or in a
|
|
40
|
+
backup keeps what it held; this changes the site as it is now.
|
|
41
|
+
</p>
|
|
42
|
+
<p class="admin-settings-buttons">
|
|
43
|
+
<button type="submit">Erase it</button>
|
|
44
|
+
<a href="{{ actionUrl }}">Cancel</a>
|
|
45
|
+
</p>
|
|
46
|
+
{% else %}
|
|
47
|
+
<p>There is nothing to erase for that address.</p>
|
|
48
|
+
<p class="admin-settings-buttons"><a href="{{ actionUrl }}">Look for another</a></p>
|
|
49
|
+
{% endif %}
|
|
50
|
+
</form>
|
|
51
|
+
{% else %}
|
|
52
|
+
<form class="admin-settings" method="post" action="{{ actionUrl }}">
|
|
53
|
+
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
54
|
+
{{ field.text('personal-data-email', fields.email, 'Email address', email,
|
|
55
|
+
type='email', error=problem) }}
|
|
56
|
+
<p class="admin-settings-buttons">
|
|
57
|
+
<button type="submit">Look for it</button>
|
|
58
|
+
</p>
|
|
59
|
+
</form>
|
|
60
|
+
{% endif %}
|
|
61
|
+
|
|
62
|
+
<p class="admin-hint">
|
|
63
|
+
Comments are found by the email they carry, so a comment whose email the
|
|
64
|
+
retention sweep has already removed, a webmention and a fediverse reply are
|
|
65
|
+
not found here. Delete those one by one from the Comments screen.
|
|
66
|
+
</p>
|
|
67
|
+
{% endblock %}
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
Resetting a colleague's password from under them is deliberately not
|
|
214
214
|
offered — a fresh account is what that is for. #}
|
|
215
215
|
{% if account.you %}
|
|
216
|
-
<form class="admin-settings" method="post" action="{{ changePasswordUrl }}">
|
|
216
|
+
<form class="admin-settings" id="change-password" method="post" action="{{ changePasswordUrl }}">
|
|
217
217
|
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
218
218
|
|
|
219
219
|
<h2>Change your password</h2>
|
package/dist/admin/comments.d.ts
CHANGED
|
@@ -67,6 +67,11 @@ export interface CommentRow {
|
|
|
67
67
|
submitted: string;
|
|
68
68
|
/** A short form of the address hash, for spotting one machine's run of them. */
|
|
69
69
|
address: string | null;
|
|
70
|
+
/**
|
|
71
|
+
* What privacy took off it, in a few words, or `null` when nothing was
|
|
72
|
+
* (TASK-135): so a moderator can tell an email removed from one never given.
|
|
73
|
+
*/
|
|
74
|
+
removed: string | null;
|
|
70
75
|
/** The post's title, or its slug when the post is not in the index. */
|
|
71
76
|
post: string;
|
|
72
77
|
/** Where the post can be read, or `null` when it is not published. */
|