Add read-only sharing, API tokens and OpenAPI spec

- Share a note as a live read-only page at /s/{token} with optional password and expiry
- Share page renders with the shared parser (TOC, footnotes, math, Mermaid, code highlighting)
  and only serves images from the note's own .assets directory
- Shares follow renames made through the API or externally
- Manage shares and API tokens from the command palette and file tree
- Accept read/write API tokens as Bearer credentials; management endpoints require a session
- Serve a hand-written openapi.json, checked against the route table in tests
This commit is contained in:
eezd committed 2026-10-17 15:21:47 +08:00
1 parent 7b076346a4
commit 5ac01894e6
26 files changed
+3147 -52

No files matched your search

+33
View File
@@ -21,6 +21,26 @@ export interface SearchResult {
snippet: string
}
export interface Share {
token: string
path: string
createdAt: number
// 0 为永久
expiresAt: number
hasPassword: boolean
// 笔记是否仍存在
exists: boolean
}
export interface ApiToken {
id: string
name: string
scope: 'read' | 'write'
createdAt: number
// 0 为从未使用
lastUsedAt: number
}
export class ApiError extends Error {
constructor(
readonly status: number,
@@ -106,6 +126,19 @@ export const api = {
return (await json<{ link: string }>(request('POST', '/api/v1/assets?note=' + encodeURIComponent(note), { body: form }))).link
},
shares: async (path?: string) =>
(await json<{ shares: Share[] }>(request('GET', '/api/v1/shares' + (path ? '?path=' + encodeURIComponent(path) : '')))).shares,
// expiresIn 为有效时长(秒),0 为永久
createShare: (path: string, password: string, expiresIn: number) =>
json<Share>(request('POST', '/api/v1/shares', { json: { path, password, expiresIn } })),
deleteShare: (token: string) => request('DELETE', '/api/v1/shares/' + encodeURIComponent(token)),
tokens: async () => (await json<{ tokens: ApiToken[] }>(request('GET', '/api/v1/tokens'))).tokens,
// 返回的 token 明文只有这一次
createToken: (name: string, scope: ApiToken['scope']) =>
json<{ token: string; info: ApiToken }>(request('POST', '/api/v1/tokens', { json: { name, scope } })),
deleteToken: (id: string) => request('DELETE', '/api/v1/tokens/' + encodeURIComponent(id)),
trash: async () => (await json<{ items: TrashItem[] }>(request('GET', '/api/v1/trash'))).items,
restore: (id: string) => json<TrashItem>(request('POST', `/api/v1/trash/${id}/restore`)),
purge: (id: string) => request('DELETE', `/api/v1/trash/${id}`),