mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-03 16:41:27 +01:00
share: the key never travels in a URL
The share page used to put the key in the API path, and every embedded
picture or video carried it in its src, so a reverse proxy's access log
held whole links. The page now sends the key in the body of
POST /api/share/{token}. The answer sets a diary_share cookie
(__Secure- over HTTPS) scoped to /api/share/{token}/, HttpOnly,
SameSite=Strict, good for twelve hours, and signed over the key's hash.
Media is served from /api/share/{token}/media/{id} only to a browser
holding that cookie, so minting a new key or unsharing voids it.
The old /api/share/{token}/{key} routes are gone; links themselves are
unchanged, since the key was always in their fragment.
This commit is contained in:
1 parent
fe1b12042d
commit
947cc2ba96
8 files changed
+257
-51
No files matched your search
@@ -19,7 +19,7 @@
|
||||
let article = $state<HTMLElement>()
|
||||
let position = $state('All')
|
||||
// Links into the private workspace come back as their text; see renderMarkdown.
|
||||
const html = $derived(doc ? renderMarkdown(doc.body, { token, key: shareKey }) : '')
|
||||
const html = $derived(doc ? renderMarkdown(doc.body, { token }) : '')
|
||||
const title = $derived(doc?.name.trim() ?? '')
|
||||
const words = $derived(doc ? doc.body.split(/\s+/).filter(Boolean).length : 0)
|
||||
// The outline gets a window of its own only when there is something to outline.
|
||||
|
||||
+1
-1
@@ -468,7 +468,7 @@ export const api = {
|
||||
backupNow: () => request<BackupStatus>('/backup', { method: 'POST' }),
|
||||
|
||||
readShared: (token: string, key: string) =>
|
||||
request<SharedDocument>(`/share/${encodeURIComponent(token)}/${encodeURIComponent(key)}`),
|
||||
request<SharedDocument>(`/share/${encodeURIComponent(token)}`, { method: 'POST', body: body({ key }) }),
|
||||
|
||||
sessions: () => request<SignedIn[]>('/sessions'),
|
||||
revokeSession: (id: string) =>
|
||||
|
||||
+8
-11
@@ -79,19 +79,16 @@ function escapeHtml(text: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render entry markdown to sanitised HTML. When share credentials are given,
|
||||
* media URLs are rewritten to the public route — token and key both, since that
|
||||
* route demands the same pair the entry itself did — so the page works for a
|
||||
* reader who has no session, and a link to another document, an address inside
|
||||
* the private workspace that would lead the reader to its login page, is kept
|
||||
* as its text.
|
||||
* Render entry markdown to sanitised HTML. On a share page, media URLs are
|
||||
* rewritten to the public route under the share's token. That route asks for
|
||||
* the cookie reading the document with its key handed out, so the key itself
|
||||
* never sits in a URL a proxy could log. A link to another document, an
|
||||
* address inside the private workspace that would lead the reader to its
|
||||
* login page, is kept as its text.
|
||||
*/
|
||||
export function renderMarkdown(source: string, share?: { token: string; key: string }): string {
|
||||
export function renderMarkdown(source: string, share?: { token: string }): string {
|
||||
const md = share
|
||||
? source.replaceAll(
|
||||
'/api/media/',
|
||||
`/api/share/${encodeURIComponent(share.token)}/${encodeURIComponent(share.key)}/media/`,
|
||||
)
|
||||
? source.replaceAll('/api/media/', `/api/share/${encodeURIComponent(share.token)}/media/`)
|
||||
: source
|
||||
|
||||
const html = marked.parse(md, { async: false }) as string
|
||||
|
||||
Reference in new issue
Block a user