docs: document new features, ops env vars, and API surface; refresh roadmap
- README: feature map updated (redirect types, protected links, QR vector export, audience analytics, API keys, admin observability, operations); API reference covers new endpoints and params. Roadmap now lists self-serve accounts, security hardening (rate limiting/CSRF/SSRF guard), Stripe billing, custom domains, and remaining power features. - INSTALL: new environment variables (WEBHOOK_URL, GEOIP_DB_PATH, GEO_API_FALLBACK, IP_ANONYMIZE, CLICK_RETENTION_DAYS, BACKUPS, BACKUP_DIR, BACKUP_KEEP) with GeoLite2 setup notes and the ip-api.com licensing caveat. - api-docs page: API-key authentication, new link/analytics/QR params, aggregate analytics, admin overview/audit/keys/purge, unlock route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+67
-11
@@ -278,10 +278,12 @@ footer {
|
||||
<div class="intro-box reveal">
|
||||
<h2>Authentication</h2>
|
||||
<p>
|
||||
All write endpoints require an active session. Log in via the web UI or call <code>POST /api/auth/login</code> with
|
||||
<code>{"username": "…", "password": "…"}</code> — the server sets an HttpOnly session cookie valid for 30 days.
|
||||
Include that cookie in all subsequent requests. Endpoints marked <strong>Admin</strong> additionally require
|
||||
the authenticated user to have admin privileges.
|
||||
Authenticated endpoints accept either of two credentials. <strong>Session cookie</strong>: log in via the web UI
|
||||
or call <code>POST /api/auth/login</code> with <code>{"username": "…", "password": "…"}</code> — the server sets
|
||||
an HttpOnly session cookie valid for 30 days. <strong>API key</strong>: create a key in the dashboard (or via
|
||||
<code>POST /api/keys</code>) and send it as <code>Authorization: Bearer qrk_…</code>. Keys carry a
|
||||
<code>read</code> or <code>write</code> scope — read-only keys may only call GET endpoints. Endpoints marked
|
||||
<strong>Admin</strong> additionally require the authenticated user to have admin privileges.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -334,6 +336,24 @@ footer {
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Returns <code>{"authenticated": true, "username": "…", "is_admin": bool, "org_id": int|null, "plan": "…", "plan_limits": {…}}</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/keys</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">List your API keys — prefix, scope, created and last-used timestamps. Full tokens are never returned.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
<td><span class="ep-path">/api/keys</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Create an API key. Body: <code>{"name": "…", "scope": "read"|"write"}</code>. The response includes the full <code>token</code> <strong>exactly once</strong> — only a hash is stored.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-delete">DELETE</span></td>
|
||||
<td><span class="ep-path">/api/keys/<span class="ep-param">:id</span></span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Revoke an API key immediately. Admins may revoke any user's key.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
@@ -362,7 +382,7 @@ footer {
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
<td><span class="ep-path">/api/shorten</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Create a short link. Body: <code>{"url": "…", "code": "…", "title": "…", "expires_at": "…", "tags": […], "is_pinned": bool}</code>. <code>code</code> is optional — omit for a random code.</td>
|
||||
<td class="ep-desc">Create a short link. Body: <code>{"url": "…", "custom_code": "…", "title": "…", "expires_at": "…", "tags": […], "redirect_type": 301|302, "password": "…"}</code>. All fields except <code>url</code> are optional — redirects default to 302; a <code>password</code> gates the link behind a branded unlock page.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
@@ -380,7 +400,7 @@ footer {
|
||||
<td><span class="method method-patch">PATCH</span></td>
|
||||
<td><span class="ep-path">/api/links/<span class="ep-param">:code</span></span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Edit link — updatable fields: <code>url</code>, <code>title</code>, <code>expires_at</code>, <code>tags</code>, <code>is_pinned</code>.</td>
|
||||
<td class="ep-desc">Edit link — updatable fields: <code>url</code>, <code>title</code>, <code>expires_at</code>, <code>tags</code>, <code>is_pinned</code>, <code>redirect_type</code> (301/302), <code>password</code> (empty string removes protection; omit to leave unchanged).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-delete">DELETE</span></td>
|
||||
@@ -392,7 +412,13 @@ footer {
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/links/<span class="ep-param">:code</span>/analytics</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Click analytics. Query: <code>?days=7|30|90</code> (capped to plan's max window). Returns <code>daily</code>, <code>referrers</code>, <code>devices</code>, <code>browsers</code>, <code>countries</code>, <code>heatmap</code> (7×24 array), and <code>max_days</code>.</td>
|
||||
<td class="ep-desc">Click analytics. Query: <code>?days=7|30|90</code> (capped to plan's max window), <code>?exclude_bots=1</code> to filter crawler/bot traffic. Returns <code>daily</code> (each day includes <code>unique</code> visitors), <code>unique_visitors</code>, <code>bot_clicks</code>, <code>referrers</code>, <code>devices</code>, <code>browsers</code>, <code>countries</code>, <code>heatmap</code> (7×24 array), and <code>max_days</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/analytics/aggregate</span></td>
|
||||
<td><span class="auth-badge auth-yes">Session</span></td>
|
||||
<td class="ep-desc">Aggregate analytics across your whole account (or org). Query: <code>?days=</code>, <code>?tag=</code> to scope to one tag, <code>?exclude_bots=1</code>. Returns <code>daily</code>, <code>unique_visitors</code>, <code>referrers</code>, <code>devices</code>, <code>browsers</code>, <code>countries</code>, and <code>top_links</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
@@ -452,19 +478,19 @@ footer {
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/qr/<span class="ep-param">:code</span></span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">QR PNG for a short link. Query params: <code>?fg=</code>, <code>?bg=</code>, <code>?size=</code>, <code>?style=</code>.</td>
|
||||
<td class="ep-desc">QR for a short link. Query params: <code>?format=png|svg|pdf</code>, <code>?ec=L|M|Q|H</code> (error correction), <code>?fg=</code>, <code>?bg=</code>, <code>?size=</code>, <code>?style=</code> (raster formats only), <code>?download=1</code> for an attachment.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/qr/custom</span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">QR PNG for any URL. Query: <code>?url=</code>, <code>?fg=</code>, <code>?bg=</code>, <code>?size=</code>, <code>?style=</code>. Styles: <code>square</code>, <code>rounded</code>, <code>dots</code>, <code>vertical</code>, <code>horizontal</code>.</td>
|
||||
<td class="ep-desc">QR for any URL. Query: <code>?url=</code> plus the same params as above (<code>format</code>, <code>ec</code>, <code>fg</code>, <code>bg</code>, <code>size</code>, <code>style</code>, <code>download</code>). Styles: <code>square</code>, <code>rounded</code>, <code>dots</code>, <code>vertical</code>, <code>horizontal</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
<td><span class="ep-path">/api/qr/custom</span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">QR PNG with logo overlay. Body: <code>{url, fg, bg, size, style, logo}</code> where <code>logo</code> is a base64-encoded image string.</td>
|
||||
<td class="ep-desc">QR with logo overlay. Body: <code>{url, fg, bg, size, style, ec, format, logo}</code> where <code>logo</code> is a base64-encoded image string (forces error correction H; SVG embeds the logo as a data URI).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
@@ -592,6 +618,30 @@ footer {
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">Mark a message as read.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/admin/overview</span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">Platform overview — totals (users, orgs, links, clicks), a 30-day click series, top links, and per-user quota usage with near-limit flags.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/admin/audit</span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">Audit log of administrative actions (user/org changes, password resets, key events, purges). Query: <code>?limit=</code> (default 200).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/api/admin/keys</span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">List all API keys across users — prefix, scope, last-used timestamp, revocation state.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
<td><span class="ep-path">/api/admin/purge</span></td>
|
||||
<td><span class="auth-badge auth-admin">Admin</span></td>
|
||||
<td class="ep-desc">Hard-delete soft-deleted links older than <code>{"days": N}</code> — frees their short codes and removes click history. Irreversible.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
@@ -650,7 +700,13 @@ footer {
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
<td><span class="ep-path">/<span class="ep-param">:code</span></span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">Redirect to the destination URL for a short code. Records click analytics (referrer, device, browser, country).</td>
|
||||
<td class="ep-desc">Redirect to the destination URL for a short code (302 by default, 301 if configured per link). Records click analytics asynchronously (referrer, device, browser, country, bot detection). Password-protected links show a branded unlock page; expired or missing links show branded visitor pages.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-post">POST</span></td>
|
||||
<td><span class="ep-path">/<span class="ep-param">:code</span>/unlock</span></td>
|
||||
<td><span class="auth-badge auth-none">—</span></td>
|
||||
<td class="ep-desc">Password submission for protected links (form field <code>password</code>). Redirects to the destination on success.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="method method-get">GET</span></td>
|
||||
|
||||
Reference in New Issue
Block a user