Files
MincedPad/docs/superpowers/specs/2026-07-14-mincedpad-design.md
2026-07-14 12:24:09 +08:00

251 lines
8.5 KiB
Markdown

# 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
- **Mobile-first responsive:** usable on phones (portrait); touch-friendly controls; composer / wall stack vertically on small screens; file pick works via mobile OS picker; no hover-only actions; Element Plus grid/breakpoints (`xs`/`sm`) for layout
### Mobile acceptance
- Home / Detail / Admin usable at ~375px width without horizontal scroll of the main layout
- Create text, upload file, copy share link, open wall item on a phone browser
- Admin login and delete/ban actions work on touch devices
## 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.
8. Phone browsers (~375px) can create, upload, view, copy links, and use admin actions without broken layout.
## 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