Files
WhatIDo/PRD.md
T
dev 104bdc4f49 feat(uploads): лимиты загрузки в env, 50 МБ на файл и 200 МБ на запись
Лимиты были захардкожены в четырёх местах фронтенда и в константах multer,
из-за чего расходились с текстами ошибок на сервере.

- UPLOAD_FILE_LIMIT_MB (50) и UPLOAD_TOTAL_LIMIT_MB (200) читаются из env;
  оба multer-конфига (upload, adminUpload) берут fileSize из них, тексты
  ошибок собираются из тех же констант вместо литералов
- UPLOAD_REQUEST_TIMEOUT_MS снимает дефолт Node в 5 минут: считается как
  UPLOAD_TOTAL_LIMIT_MB * 7500, иначе 200 МБ по мобильной сети не успевают
- GET /api/public-settings отдаёт upload_file_limit_mb / upload_total_limit_mb,
  фронтенд читает их вместо собственных констант

Проверено на живом стеке: 20 МБ и 180 МБ суммарно принимаются, 55 МБ и
225 МБ отклоняются с верными сообщениями, скачивание 45 МБ из S3 совпадает
по sha256 с оригиналом, api.smoketest.js — 57 PASS / 0 FAIL.
2026-10-03 10:38:36 +03:00

291 lines
12 KiB
Markdown

# PRD.md — Product Requirements Document: WhatIDo
**Version**: 1.0
**Status**: Active
**Last Updated**: 2026-09-09
---
## 1. Product Summary
**WhatIDo** is a self-hosted accounting system for an educational center (youth club, coding school, art studio). It tracks attendance, student project works, group photo chronicles, and publishes public showcase pages via share links. Designed for zero-public-IP deployment using Tailscale Funnel.
**Target Users**:
- **Administrators/Teachers** — manage groups, students, entries, files, settings
- **Parents/Students** — view public showcase pages (read-only, no auth)
**Core Value**: Simple, secure, zero-infrastructure publishing. Runs on any Linux box with Docker.
---
## 2. Functional Requirements
### 2.1 Groups Management
| ID | Requirement | Priority |
|----|-------------|----------|
| GRP-1 | CRUD groups: name, schedule (day of week, start/end time), branch assignment | Must |
| GRP-2 | Group cover photo (auto-set from latest group photo or manual) | Must |
| GRP-3 | Photo chronicle per group: upload, caption, date taken, pagination | Must |
| GRP-4 | Active groups endpoint (filters by current day/time in Europe/Moscow) | Must |
| GRP-5 | Branches (locations): CRUD with address, phone; groups link to branch | Should |
### 2.2 Students Management
| ID | Requirement | Priority |
|----|-------------|----------|
| STU-1 | CRUD students: name, group assignment | Must |
| STU-2 | Batch assign students to group | Should |
| STU-3 | Unique name constraint per student | Must |
### 2.3 Journal Entries (Attendance + Project Works)
| ID | Requirement | Priority |
|----|-------------|----------|
| ENT-1 | Create entry: student name (free text), group, description, photo, multiple files | Must |
| ENT-2 | List entries with filters: group, date range, student name, search (name/description), deleted flag | Must |
| ENT-3 | Update entry: description, photo, files | Must |
| ENT-4 | Soft delete / restore (deleted_at timestamp) | Must |
| ENT-5 | Trash view: list deleted entries, restore, permanent delete | Must |
| ENT-6 | Pagination (limit/offset) + total count | Must |
| ENT-7 | Anti-spam: min interval between entries per student (configurable, default 30 min) | Must |
| ENT-8 | Files attached to entry: upload (max 10 files, per-file and total size limits from `UPLOAD_FILE_LIMIT_MB`/`UPLOAD_TOTAL_LIMIT_MB`), download by token | Must |
### 2.4 Files Management (Centralized)
| ID | Requirement | Priority |
|----|-------------|----------|
| FIL-1 | List all files with filters: search, student, group, date range | Must |
| FIL-2 | Detached files tab: files with `entry_id = NULL` | Must |
| FIL-3 | Detach file from entry (sets `detached_at`) | Must |
| FIL-4 | Delete file (removes from disk + DB) | Must |
| FIL-5 | Public file access by token (image inline, others download) | Must |
| FIL-6 | File size display in list | Should |
### 2.5 Share Links (Public Showcase Pages)
| ID | Requirement | Priority |
|----|-------------|----------|
| SHR-1 | Create share link: name, optional group, student, date range, anonymize names, expiry (default 7 days), optional password | Must |
| SHR-2 | List/Edit/Delete share links (admin) | Must |
| SHR-3 | Public page (`/s/:token`): shows filtered entries + group photos | Must |
| SHR-4 | Password protection on share link (bcrypt) | Must |
| SHR-5 | Expiry enforcement (410 Gone after expires_at) | Must |
| SHR-6 | Anonymize student names on public page (Student 1, Student 2...) | Should |
| SHR-7 | Group photos on public page (latest 12) | Should |
| SHR-8 | File download from share page (validates link + password + filters) | Must |
### 2.6 Dashboard & Statistics
| ID | Requirement | Priority |
|----|-------------|----------|
| DSH-1 | Stats cards: total entries, trash count, groups, unique students | Must |
| DSH-2 | Active groups right now (schedule match) | Must |
| DSH-3 | Activity chart: entries per day (last 14 days) | Should |
| DSH-4 | Top students by entry count | Should |
| DSH-5 | Recent entries list (last 10) | Should |
### 2.7 Settings
| ID | Requirement | Priority |
|----|-------------|----------|
| SET-1 | Footer left/right text (displayed on public pages) | Must |
| SET-2 | Anti-spam interval (minutes) | Must |
| SET-3 | Admin-only access | Must |
### 2.8 Backup & Restore
| ID | Requirement | Priority |
|----|-------------|----------|
| BAK-1 | Download full backup: tar.gz with data.json (all tables) + uploads/ | Must |
| BAK-2 | Restore from backup file: validates format, replaces all data, resets sequences | Must |
| BAK-3 | Host-level scripts: `backup.sh`, `restore.sh` | Should |
| BAK-4 | Audit log entry for backup download/restore | Must |
### 2.9 Audit Log
| ID | Requirement | Priority |
|----|-------------|----------|
| AUD-1 | Log all mutating actions: action name, target JSON, IP, timestamp | Must |
| AUD-2 | Admin view: paginated list (default 100, max 1000) | Must |
### 2.10 Security & Infrastructure
| ID | Requirement | Priority |
|----|-------------|----------|
| SEC-1 | Admin auth via `X-Admin-Token` header (env `ADMIN_PASSWORD`, no default) | Must |
| SEC-2 | Rate limiting: entries 10/15min, files/share 300/15min | Must |
| SEC-3 | Upload validation: block dangerous extensions, MIME check, size limits | Must |
| SEC-4 | Path traversal protection on file delete/serve | Must |
| SEC-5 | Helmet headers (X-Frame-Options, nosniff, HSTS, Referrer-Policy) | Must |
| SEC-6 | No CORS (cross-origin blocked) | Must |
| SEC-7 | DB port not exposed publicly | Must |
| SEC-8 | TLS termination by Tailscale (Let's Encrypt), app uses self-signed cert internally | Must |
| SEC-9 | Tailscale Funnel publication (no public IP, no port forward) | Must |
---
## 3. Non-Functional Requirements
| Category | Requirement |
|----------|-------------|
| **Performance** | API responses < 500ms for typical queries; pagination for large lists |
| **Reliability** | DB healthcheck; app restarts on crash; uploads persisted on host |
| **Scalability** | Single-instance design; PostgreSQL connection pooling via `pg.Pool` |
| **Maintainability** | Single `server.js` file; vanilla frontend; no build step |
| **Portability** | Docker Compose; runs on any Linux/ARM64/AMD64 with Docker |
| **Backup/Recovery** | Full restore < 5 min for typical dataset (< 1 GB) |
| **Security** | No secrets in image; env vars only; regular dependency updates |
---
## 4. Data Model
```
groups
id PK, name UK, created_at, day_of_week (0-6), time_start, time_end, branch_id FK, cover_path
students
id PK, name UK, group_id FK, created_at
entries
id PK, student_name, group_id FK, description, photo_path, deleted_at, created_at
project_files
id PK, entry_id FK (nullable), token UK, path, name, created_at, detached_at
group_photos
id PK, group_id FK, photo_path, caption, taken_at, created_at
share_links
id PK, token UK, name, group_id FK, student_name, date_from, date_to,
anonymize_names, expires_at, access_password_hash, created_at
settings
key PK, value
audit_log
id PK, action, target JSONB, ip, created_at
branches
id PK, name UK, address, phone, created_at
```
---
## 5. API Surface (Key Endpoints)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/entries` | Admin | List entries (filters, pagination) |
| POST | `/api/entries` | Admin | Create entry (photo + files) |
| PUT | `/api/entries/:id` | Admin | Update entry |
| DELETE | `/api/entries/:id` | Admin | Soft delete |
| POST | `/api/entries/:id/restore` | Admin | Restore from trash |
| GET | `/api/files` | Admin | All files (filters) |
| GET | `/api/files/detached` | Admin | Detached files |
| POST | `/api/files/:id/detach` | Admin | Detach file |
| GET | `/api/files/:token` | Public | Download file by token |
| GET | `/api/groups` | Public | List groups |
| POST/PUT/DELETE | `/api/groups` | Admin | CRUD groups |
| GET/POST | `/api/groups/:id/photos` | Admin | Group photo chronicle |
| GET | `/api/share/:token` | Public | Share page data |
| GET | `/api/links` | Admin | List share links |
| POST/PUT/DELETE | `/api/links` | Admin | CRUD share links |
| GET | `/api/backup` | Admin | Build & download backup (compat) |
| POST | `/api/backup` | Admin | Build backup, returns download URL |
| GET | `/api/backup/:token` | Token | Download built backup (resumable) |
| POST | `/api/restore` | Admin | Upload & restore backup |
| GET | `/api/dashboard` | Admin | Dashboard data |
| GET | `/api/stats` | Admin | Stats cards |
| GET | `/api/audit` | Admin | Audit log |
---
## 6. User Flows
### 6.1 Teacher Creates Attendance Entry
1. Opens `/journal.html`
2. Fills form: student name (typeahead from existing), group, description
3. Adds photo (optional) + project files (optional)
4. Submits → entry appears in list, files accessible by token
### 6.2 Admin Publishes Showcase for Parents
1. Opens `/links.html`
2. Creates share link: selects group, date range, sets password
3. Copies link `https://whatido.tailnet.ts.net/s/abc123`
4. Sends to parents → they open, enter password, view entries + photos
### 6.3 Admin Restores from Backup
1. Opens `/settings.html` → Backups tab
2. Uploads `.tar.gz` backup file
3. Confirms → all data replaced, sequences reset, orphans cleaned
---
## 7. Deployment Architecture
```
Internet / Tailnet
│
▼
┌──────────────────┐
│ Tailscale │ (network_mode: host, port 443)
│ Funnel/Serve │ TLS: Let's Encrypt (*.ts.net)
└────────┬─────────┘
│ HTTPS (trusts app self-signed cert)
▼
┌──────────────────┐
│ App (Node.js) │ 127.0.0.1:3443 (HTTPS), :3003 (HTTP→HTTPS redirect)
│ Express + pg │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ PostgreSQL 16 │ Internal docker network only
│ (named volume) │
└──────────────────┘
Host filesystem:
./uploads ──────► /app/uploads (bind mount)
./certs ──────► /etc/tailscale/app-certs (ro)
```
---
## 8. Configuration
| Variable | Required | Description |
|----------|----------|-------------|
| `ADMIN_PASSWORD` | Yes | Admin token value (no default, server refuses start) |
| `DB_PASSWORD` | Yes | Postgres `app` user password |
---
## 9. Release Criteria
- [ ] All Must-have requirements implemented and tested
- [ ] Docker Compose starts cleanly on fresh host (`docker compose up -d --build`)
- [ ] Tailscale Funnel publishes successfully (manual verification)
- [ ] Backup/restore roundtrip works (data + files intact)
- [ ] No critical security findings (rate limits, upload validation, auth)
- [ ] README.md updated with accurate setup instructions
---
## 10. Future Considerations (Not in Scope v1)
- Multi-user auth (teachers with own logins)
- Email/push notifications
- Mobile app / PWA
- Rich text editor for descriptions
- Bulk import students (CSV)
- Webhooks for external integrations
- Automated scheduled backups to S3/remote
- Role-based access (read-only vs admin)
---
## 11. Acceptance Test Scenarios
| Scenario | Steps | Expected |
|----------|-------|----------|
| Fresh deploy | `cp .env.example .env` → edit → `docker compose up -d --build` | App healthy, DB migrated, HTTPS on 3443 |
| Create entry | POST `/api/entries` with photo + 2 files | Entry created, files downloadable by token |
| Share link | Create link with password → open `/s/token` → enter password | Entries filtered, photos shown, files download |
| Backup/restore | Download backup → delete entry → restore → verify entry back | Full state restored, sequences correct |
| Tailscale publish | `tailscale up` → enable Funnel → `tailscale funnel` | Public URL accessible via HTTPS |
| Upload rejection | POST `.html` file → 400 error | Dangerous extensions blocked |
| Rate limit | 11 rapid POST `/api/entries` → 429 on 11th | Entry limiter enforced |
---