Add MincedPad design spec for self-hosted share notepad.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
# MincedPad Design Spec
|
||||
|
||||
**Date:** 2026-07-14
|
||||
**Project:** MincedPad — 自托管公共数据共享与记事本平台
|
||||
**Status:** Draft for review
|
||||
|
||||
## 1. Overview
|
||||
|
||||
MincedPad is a lightweight, self-hosted web tool for anonymously pasting text and uploading files, then sharing via a public wall and/or a direct link. No registration is required for visitors. An admin password gate provides moderation (delete content, ban IPs).
|
||||
|
||||
### Goals
|
||||
|
||||
- Zero-login create / share / view
|
||||
- Public wall by default, optional link-only
|
||||
- Markdown text + common file upload/preview/download
|
||||
- TTL (default 24h) and burn-after-read
|
||||
- Docker Compose one-shot deploy, low resource use
|
||||
- Rate limit, upload size cap, admin moderation
|
||||
|
||||
### Non-goals (v1)
|
||||
|
||||
- Multi-admin RBAC / audit export
|
||||
- Multi-node / object-storage backends
|
||||
- Real-time WebSocket wall updates
|
||||
- End-to-end encryption (content is readable by the server)
|
||||
|
||||
## 2. Decisions (confirmed)
|
||||
|
||||
| Topic | Choice |
|
||||
| --- | --- |
|
||||
| Name | MincedPad |
|
||||
| Stack | FastAPI + Vue3 + Element Plus SPA |
|
||||
| Architecture | Monorepo; frontend built into backend static hosting; one container |
|
||||
| Storage | SQLite metadata + local `data/uploads/` files |
|
||||
| Visibility | Default public on wall; optional “link only” |
|
||||
| Expiry | Default 24h; options 1h / 24h / 7d / never; optional burn-after-read |
|
||||
| Admin | Single password via env → JWT |
|
||||
| Rate limit | 2 create/upload requests per IP per second |
|
||||
| Max file | 200 MB (env configurable) |
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
```
|
||||
Browser (Vue3 + Element Plus)
|
||||
│
|
||||
▼
|
||||
FastAPI (single process)
|
||||
├── /api/* JSON API
|
||||
├── /admin UI routes (SPA)
|
||||
├── /* SPA static (dist/)
|
||||
├── SQLite data/mincedpad.db
|
||||
└── Files data/uploads/<random>
|
||||
└── Cleaner thread expire + burn cleanup
|
||||
```
|
||||
|
||||
### Repository layout
|
||||
|
||||
```
|
||||
MincedPad/
|
||||
├── backend/
|
||||
│ ├── app/
|
||||
│ │ ├── main.py
|
||||
│ │ ├── config.py
|
||||
│ │ ├── db.py
|
||||
│ │ ├── models.py
|
||||
│ │ ├── schemas.py
|
||||
│ │ ├── api/
|
||||
│ │ │ ├── public.py
|
||||
│ │ │ └── admin.py
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── items.py
|
||||
│ │ │ ├── storage.py
|
||||
│ │ │ ├── rate_limit.py
|
||||
│ │ │ ├── bans.py
|
||||
│ │ │ └── cleaner.py
|
||||
│ │ └── security.py
|
||||
│ ├── requirements.txt
|
||||
│ └── Dockerfile
|
||||
├── frontend/
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.ts
|
||||
│ └── src/
|
||||
│ ├── views/ # Home, ItemDetail, Admin
|
||||
│ ├── api/
|
||||
│ └── components/
|
||||
├── data/ # runtime volume (gitignored)
|
||||
├── docker-compose.yml
|
||||
├── README.md
|
||||
└── docs/superpowers/specs/
|
||||
```
|
||||
|
||||
### Runtime
|
||||
|
||||
- Default listen `:8080`
|
||||
- Compose mounts `./data:/data`
|
||||
- Env: `ADMIN_PASSWORD`, `SECRET_KEY`, `MAX_UPLOAD_MB=200`, `RATE_LIMIT_PER_SECOND=2`
|
||||
|
||||
## 4. Data model
|
||||
|
||||
### `items`
|
||||
|
||||
| Column | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| id | INTEGER PK | |
|
||||
| slug | TEXT UNIQUE | short public id (e.g. nanoid) |
|
||||
| kind | TEXT | `text` \| `file` |
|
||||
| title | TEXT | optional; default from first line or filename |
|
||||
| body | TEXT NULL | markdown/plain for text items |
|
||||
| file_name | TEXT NULL | original name |
|
||||
| file_path | TEXT NULL | relative path under uploads |
|
||||
| mime | TEXT NULL | |
|
||||
| size_bytes | INTEGER NULL | |
|
||||
| is_public | BOOLEAN | true = on wall; false = link only |
|
||||
| burn_after_read | BOOLEAN | |
|
||||
| expires_at | DATETIME NULL | null = never |
|
||||
| created_ip | TEXT | |
|
||||
| created_at | DATETIME | |
|
||||
| view_count | INTEGER | |
|
||||
| burned | BOOLEAN | true after burn consumed |
|
||||
|
||||
### `banned_ips`
|
||||
|
||||
| Column | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| ip | TEXT PK | |
|
||||
| reason | TEXT | |
|
||||
| created_at | DATETIME | |
|
||||
|
||||
Admin auth uses signed JWT (HS256) with `SECRET_KEY`; no separate sessions table in v1.
|
||||
|
||||
## 5. API
|
||||
|
||||
### Public
|
||||
|
||||
| Method | Path | Description |
|
||||
| --- | --- | --- |
|
||||
| POST | `/api/items` | Create text item (JSON) |
|
||||
| POST | `/api/items/upload` | Create file item (multipart) |
|
||||
| GET | `/api/items/wall` | Paginated public wall (`page`, `page_size`) |
|
||||
| GET | `/api/items/{slug}` | Get item metadata + text body; may burn |
|
||||
| GET | `/api/items/{slug}/file` | Download/inline file |
|
||||
|
||||
Create body (text) fields: `body`, `title?`, `is_public` (default true), `burn_after_read` (default false), `ttl` (`1h`\|`24h`\|`7d`\|`never`, default `24h`).
|
||||
|
||||
Upload form fields: file + same options.
|
||||
|
||||
Errors: `413` too large, `429` rate limited, `403` banned IP (writes), `404` missing/expired/burned.
|
||||
|
||||
### Admin (Bearer JWT)
|
||||
|
||||
| Method | Path | Description |
|
||||
| --- | --- | --- |
|
||||
| POST | `/api/admin/login` | `{ password }` → `{ token }` |
|
||||
| GET | `/api/admin/items` | All items, newest first |
|
||||
| DELETE | `/api/admin/items/{id}` | Force delete + file |
|
||||
| GET | `/api/admin/bans` | List bans |
|
||||
| POST | `/api/admin/bans` | `{ ip, reason? }` |
|
||||
| DELETE | `/api/admin/bans/{ip}` | Unban |
|
||||
|
||||
## 6. Frontend
|
||||
|
||||
### Pages
|
||||
|
||||
1. **Home `/`**
|
||||
- Composer: Markdown textarea (Element Plus) + drag-and-drop upload zone
|
||||
- Options: TTL select (default 24h), burn-after-read switch, link-only switch
|
||||
- Actions: “发布文本” / 上传后自动生成结果
|
||||
- Result: copyable share URL
|
||||
- Wall: recent public items (poll every ~15s or on focus)
|
||||
|
||||
2. **Detail `/p/:slug`**
|
||||
- Text: Markdown render
|
||||
- Image MIME: inline preview
|
||||
- Other files: download button (+ optional content-disposition inline for pdf when safe)
|
||||
- Burn warning before first view if applicable
|
||||
|
||||
3. **Admin `/admin`**
|
||||
- Password login
|
||||
- Table of items with delete
|
||||
- Ban IP form + ban list with unban
|
||||
|
||||
### UX constraints
|
||||
|
||||
- Anonymous-first: landing page is immediately editable
|
||||
- Clear toasts for size/rate/ban failures
|
||||
- Friendly 404 for expired/burned content
|
||||
|
||||
## 7. Security & limits
|
||||
|
||||
- **Upload cap:** default 200 MB via `MAX_UPLOAD_MB`
|
||||
- **Rate limit:** 2 create/upload ops per IP per second (in-memory sliding window; acceptable for single-node)
|
||||
- **Ban list:** banned IPs cannot create/upload; wall and read still allowed unless later tightened
|
||||
- **Filename:** store as random UUID + safe extension; never trust client path
|
||||
- **Extension allowlist (v1):** common images (`png/jpg/jpeg/gif/webp/svg`), text/docs (`txt/md/pdf/doc/docx/xls/xlsx/ppt/pptx/csv`), archives (`zip/7z/tar/gz/rar`), plus `json`
|
||||
- **Admin:** `ADMIN_PASSWORD` required at startup in production; JWT expiry 12h
|
||||
|
||||
## 8. Background cleanup
|
||||
|
||||
A lightweight loop (async task or thread) every 60s:
|
||||
|
||||
1. Delete items where `expires_at < now`
|
||||
2. Delete burned items already marked and past grace (immediate delete on burn read is OK)
|
||||
3. Remove orphan files if item row gone
|
||||
|
||||
## 9. Docker / deploy
|
||||
|
||||
```yaml
|
||||
# conceptual
|
||||
services:
|
||||
mincedpad:
|
||||
build: .
|
||||
ports: ["8080:8080"]
|
||||
environment:
|
||||
ADMIN_PASSWORD: "change-me"
|
||||
SECRET_KEY: "change-me"
|
||||
MAX_UPLOAD_MB: "200"
|
||||
RATE_LIMIT_PER_SECOND: "2"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Multi-stage Dockerfile: build frontend → copy `dist` into backend image → run uvicorn.
|
||||
|
||||
## 10. Acceptance criteria (v1)
|
||||
|
||||
1. Unauthenticated users can create text and upload files and receive a share URL.
|
||||
2. New items are on the public wall unless “link only” is set.
|
||||
3. Default TTL is 24h; 1h / 7d / never and burn-after-read work.
|
||||
4. Markdown renders; images preview; other allowed types download.
|
||||
5. Admin can log in with env password, delete items, ban/unban IPs.
|
||||
6. Uploads over 200 MB rejected; more than 2 create/upload requests per second per IP get 429.
|
||||
7. `docker compose up -d` brings up a working instance on port 8080.
|
||||
|
||||
## 11. Implementation order (for planning)
|
||||
|
||||
1. Backend skeleton: config, DB models, create/get/wall, file storage
|
||||
2. Rate limit + bans + cleaner
|
||||
3. Admin auth + moderation APIs
|
||||
4. Vue pages: Home, Detail, Admin
|
||||
5. Docker multi-stage + compose + README
|
||||
6. Smoke tests / manual acceptance checklist
|
||||
Reference in New Issue
Block a user