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