Пайплайн людей v1.6 — ролі, реєстрація, доступ, схвалення даних
Версія: v1.6 · 2026-06-01 · ✅ погоджено
Як юзери з'являються на платформі, що можуть, як видаляються, як їхні дані потрапляють у BLUP-аналіз через схвалення. Цей документ — специфікація для імплементації, не поточний стан коду (частина рішень ще не закодована — позначено як 🚧 to do).
Дві осі ролей
На платформі дві окремі осі. Людина може бути власником своєї пасіки і одночасно помічником на чужій.
Ось A: platform-level (users.role)
| Роль | Хто це | Що може |
|---|---|---|
| admin | Розробник платформи | Все. Реєструє senior'ів, бачить усі пасіки/спілки, перемикається в «👁 як пасічник» |
| senior | Старший спілки (1 на спілку) | Реєструє пасічників своєї спілки, підтверджує заявки, керує breeder_number у межах спілки |
| beekeeper | Пасічник | Працює зі своїми пасіками. Дефолт |
Ось B: per-apiary (user_apiaries.role)
Контекст конкретної пасіки. beekeeper може мати різні ролі на різних пасіках одночасно.
| Роль | Що може на цій пасіці |
|---|---|
| owner 🏠 | Повний контроль: CRUD структури, запрошення помічників. ОДИН на пасіку (CD-008) |
| manager 🛡 (Уповноважений) | Те ж що owner, окрім видалення пасіки і передачі ownership |
| helper 🤝 (Помічник) | Огляди, друк QR. Не CRUD структури |
Beekeeper без власної пасіки — валідний кейс. Сезонний/найманий
працівник, помічник у татка-пасічника. У БД: users.role=beekeeper,
0 рядків як owner у user_apiaries, ≥1 рядків як helper.
Статуси (users.status)
| Статус | Що означає |
|---|---|
pending |
Подав заявку на самореєстрацію, чекає підтвердження senior/admin |
active |
Підтверджений, повний доступ |
rejected |
Відхилений senior'ом АБО soft-deleted (архів) |
Спілки (associations)
Спілка = група пасічників на одного senior. Перший senior = адмін платформи (зараз — Віталій). При зростанні мережі — кожна нова група має свого senior'a.
| Поле | Що це |
|---|---|
code |
001, 002, … (3 цифри, generic — без прив'язки до інституції) |
name |
Опціональна назва («Кафедра аграрного інституту», «Полтавська група», …) |
senior_id |
users.id старшого спілки (не Telegram — це legacy назва колонки PRIMARY KEY) |
Поточний стан (2026-06-01): одна спілка 001 (буде створена при
імплементації), senior = Vitaliy Andriiuk.
breeder_number присвоюється послідовно у межах спілки: breeder №1
у спілці 001 — це Vitaliy. Наступний owner у спілці 001 → №2. У спілці
002 нумерація почнеться з 1 знову.
Як юзер потрапляє на платформу
Хто може самореєструватися (через сайт)
| Роль | Self-register? | Через кого |
|---|---|---|
| admin | ❌ ні | Через env ADMIN_IDS (один) |
| senior | ❌ ні | Реєструє admin |
| owner | ✅ так | Сам через сайт АБО senior |
| manager | ❌ ні | Запрошує owner через magic-link |
| helper | ❌ ні | Запрошує owner через magic-link |
Логіка: owner-flow має low friction (пасічник прийшов сам — пускаємо після перевірки senior'ом). Інші ролі — invitation-only для уникнення спаму і анонімних helper'ів.
Шлях 1: self-register owner'а
1. Юзер: beeu.com.ua/register
2. Форма: email, ПІБ, телефон, назва пасіки (опційно)
3. POST /api/register
→ users INSERT (role=beekeeper, status=pending)
→ apiaries INSERT (owner_id=новий uid, name=...)
→ senior спілки 001 отримує сповіщення (email)
4. Senior відкриває /dashboard → «📋 Заявки» → ✓ або ✕
→ ✓: status=active + breeder_number присвоєно
→ ✕: status=rejected (юзер бачить «Заявку відхилено»)
5. Юзер отримує email: «Прийнято / Відхилено»
6. Логінится через email + password (або magic-link)
Шлях 2: senior реєструє owner'а вручну
1. Senior → /dashboard → «👤 Користувачі» → «➕ Додати owner'a»
2. Форма: email, ПІБ, телефон, назва пасіки
3. POST /dashboard/add-user
→ users INSERT (role=beekeeper, status=active відразу)
→ apiaries INSERT (owner_id=новий uid)
→ breeder_number присвоєно
4. Юзер отримує email з посиланням для першого входу
(magic-link, embedded — щоб задав пароль)
Шлях 3: owner запрошує manager/helper
1. Owner → /dashboard → Пасіки → «➕ Запросити помічника»
2. Форма: роль (manager/helper) + ОПЦІЇ доставки:
a) Email: ввести email запрошуваного → платформа шле лист
b) Copy link: згенерувати лінк, скопіювати, відправити
будь-яким мессенджером (WhatsApp/Viber/Signal/Telegram особисто)
3. Сервер генерує invitation токен:
→ invitations INSERT (token, apiary_id, role, expires_at, invited_by)
→ URL: beeu.com.ua/invite/{token}
4. Запрошуваний відкриває посилання → форма:
- Якщо вже зареєстрований (email знайдено) → залогінитись
→ автоматично додається у пасіку з вказаною роллю
- Якщо ні → реєструється (email, ПІБ, телефон) → status=active
одразу (бо вже запрошений owner'ом, approval не треба)
→ додається у пасіку
5. Токен інвалідовано
Чому email + copy link, без Telegram: Telegram-залежність прибрана. Email безкоштовний (Gmail SMTP / Sendgrid free tier), а copy-link дає owner'у гнучкість переслати через будь-який зручний канал.
Шлях 4: admin реєструє senior'а
1. Admin → /dashboard → «🧑🏫 Спілки» → «➕ Додати senior'a»
2. Форма: email, ПІБ, code спілки (002, 003, ...)
3. Backend:
→ users INSERT (role=senior, status=active)
→ associations INSERT (code, senior_id)
4. Email senior'у з посиланням для першого входу
Логін
Два способи (юзер обирає):
A) Email + password (стандарт)
/login→ email + пароль → авторизовано- «Забув пароль?» → magic-link на email (one-time, 10 хв)
- Пароль зберігається у БД як bcrypt-хеш
B) Magic-link / passwordless (опція)
/login→ тільки email → «🔗 Надіслати посилання»- Лист з посиланням
beeu.com.ua/auth/{token}(одноразовий, 10 хв) - Клік → залогінено
- Пароль не потрібен взагалі
Безпека magic-link: Знання чужого email НЕ дає доступ. Посилання приходить тільки у поштову скриньку власника email. Якщо пошта захищена (Google/iCloud password + 2FA) — magic-link рівно настільки ж безпечний як email+password.
| Атака | Email+password | Magic-link |
|---|---|---|
| Знає email | ⚠️ може брутфорсити пароль | ✅ безпечно |
| Фішинг пароля | ❌ зайде | ✅ нема пароля |
| Витік бази | ⚠️ хеш можна зламати | ✅ нема паролів |
| Злам пошти | ❌ reset → зайде | ❌ link → зайде |
Висновок: обидва способи мають одну спільну поверхню атаки — безпека пошти. Magic-link просто не вдає що пароль додає захист.
Захист посилання: - Час життя — 10 хв - Одноразове - HTTPS-only - Опційно 2FA для admin/senior (через SMS чи Authenticator app)
Що може юзер після реєстрації
Beekeeper-owner
- CRUD вуликів/маток своєї пасіки
- Огляди через PWA, друк QR
- Запрошує managers / helpers через magic-link
- /dashboard: аналітика, баланс гнізда, польовий план
- Видаляє своїх manager/helper з пасіки
Beekeeper-manager
- Все що owner, окрім:
- видалення пасіки
- передачі ownership
- запрошення інших manager'ів (їх запрошує тільки owner)
- ✅ Може запрошувати helper'ів у пасіку
Beekeeper-helper
- Бачить пасіку де доданий
- Додає огляди, друкує QR
- ❌ Не CRUD структури, не запрошує інших
Senior
- Бачить дашборд спілки 00X — заявки на реєстрацію
- ✓/✕ approve
- Присвоює
breeder_number(автоматично при approve) - Бачить усі пасіки своєї спілки (read-only — не керує)
- ❌ Не бачить чужі спілки
Admin
- Усе вище
- Реєструє/видаляє senior'ів
- Бачить усі спілки, усі пасіки
- Toggle «👁 як пасічник»
Схвалення даних (BLUP-quality gate)
Мотивація: дисертація / BLUP/EBV пайплайн вимагає чистих даних. Помилки помічника (приклад: «сила 50 рамок» при норма 8-15) не повинні впливати на племінні цінності. Schвалення — gate-keeper якості.
Два рівні валідації
Рівень 1: платформа (автомат при save)
- Числові межі per field (CD-перевіримо в protokoly_otsinjuvannia.md)
- Sanity checks: colony_strength ≤ 30, varroa_percent ≤ 100, тощо
- Soft warnings («сила 30+? перевір») — не блокує save
- Hard rejects (від'ємні значення, текст у числовому полі) — блокує
Рівень 2: схвалення людиною (workflow per role)
┌──────────────┐
PWA save → status='draft' ────▶ visit_records │
└──────┬───────┘
│
┌────────────────────┴─────────────────────┐
▼ ▼
Автор = owner пасіки? Автор = helper / manager?
автоматично approved status='pending'
↓
Owner відкриває «📋 Перевірити»
↓
✓ approve ✕ reject
↓
status='approved' або 'rejected'
↓
Senior спілки
бачить агрегат
(опц. другий approve)
Хто кого схвалює
| Запис від | Хто схвалює | Як |
|---|---|---|
| Owner своєї пасіки | Автоматично approved при save | Owner = власник даних, довіра 100% |
| Manager пасіки | Owner пасіки | «📋 Записи помічників → ✓/✕» |
| Helper пасіки | Owner пасіки | те ж саме |
| Senior для своїх даних | Автоматично approved (він власник своїх) | |
| Senior як 2-й рівень | Опц. перевіряє approved записи спілки | «📊 Записи спілки → 🔍 audit» |
Колонки в БД (вже існують з 0006)
visit_records.data_status TEXT CHECK IN ('draft','pending','approved','rejected')
visit_records.approved_by BIGINT REFERENCES users
visit_records.approved_at TIMESTAMPTZ
Те ж саме для queens.
Поточний стан: усі записи draft (нічого не використовується).
v1.6 запускає workflow.
BLUP/EBV pipeline filter
-- BLUP бере тільки approved дані:
SELECT * FROM visit_records WHERE data_status = 'approved'
Це жорсткий gate — жоден draft/pending/rejected запис не йде в племінні цінності. Гарантує якість.
UI компоненти 🚧
На дашборді owner'а: - 📋 Бейдж «N записів від помічників чекають» - Сторінка «Перевірити записи помічників»: - Список pending з фільтром по даті/помічнику - Кожен запис: ✓ Approve · ✕ Reject (опц. коментар чому) - Bulk approve (5-10 за раз)
На дашборді senior'а: - 📊 Огляд спілки: approved/pending/rejected per пасіка - 🔍 Audit-перегляд: відкрити будь-який approved запис, перевірити - ⚠️ Re-reject: senior може повернути approved→rejected (рідко)
На дашборді helper'а: - 📌 Бейдж «N ваших записів чекає схвалення» - 📌 «N відхилено» (можна побачити коментар owner'а) - Прозорість: бачить статус своїх записів
Що НЕ робимо
- ❌ Auto-approve по таймауту (наприклад «7 днів без дії → approved»): ризик пропустити сміття. Краще нагадування owner'у.
- ❌ Schвалення кожного оглядового рядка окремо: тільки visit-цілісно.
- ❌ Helper не схвалює нічого (тільки створює)
- ❌ Сторонній (не owner, не senior) — не схвалює
Edge cases
- Owner видалив помічника після того як той зробив pending записи → записи лишаються pending, owner може дивитись і approve/reject як завжди
- Manager робить запис → approve owner. Це інтенційно: manager це «майже owner», але контроль все одно у owner'а
- Helper робить запис на пасіці А, де він helper, потім пасіка передана новому owner'у → новий owner успадковує queue pending
Видалення (soft-delete)
Beekeeper з пасіки (per-apiary)
Owner → Пасіки → ✕ біля helper/manager
→ DELETE FROM user_apiaries WHERE user_id=? AND apiary_id=?
→ Юзер втрачає доступ до пасіки
→ Огляди ЗАЛИШАЮТЬСЯ (recorded_by лишається той же uid)
Beekeeper повністю (платформа)
Senior спілки 00X → Користувачі → «🗄 Архівувати»
→ UPDATE users SET status='rejected' WHERE id=?
→ Юзер не може логінитись (login form каже «Обліковий запис архівовано»)
→ Усі огляди + структури ЗАЛИШАЮТЬСЯ (data integrity)
→ Можна повернути: UPDATE status='active'
Hard-delete (DELETE FROM users) — тільки SQL admin'ом у крайніх випадках (помилкова реєстрація, GDPR-запит). Каскади ON DELETE SET NULL зберігають історичні дані (recorded_by стає NULL).
Senior спілки
Якщо senior йде → admin призначає нового перед видаленням старого. Спілка не може лишитись без senior'а.
Що треба закодити (🚧 to do)
Phase 1: Foundation (✅ зроблено 2026-06-01)
| # | Завдання | Статус |
|---|---|---|
| 1 | Міграція 0085: users.email/password_hash/email_verified_at |
✅ |
| 2 | Міграція 0085: invitations таблиця |
✅ |
| 3 | Seed: асоціація 001 + Vitaliy як senior |
✅ |
| 4 | app/auth.py (bcrypt + invitation CRUD) |
✅ |
| 5 | app/emails.py (Resend client + 4 шаблони) |
✅ |
Phase 3: Auth endpoints (🚧 наступне)
| # | Завдання | Підстава |
|---|---|---|
| 6 | Endpoint /api/register (self-register owner) |
Шлях 1 |
| 7 | Endpoint /api/login (email+password) |
Логін B |
| 8 | Endpoint /api/auth/{token} (magic-link login) |
Логін C |
| 9 | Endpoint /api/forgot-password |
Reset через magic-link |
| 10 | Endpoint /api/verify-email |
Шлях 1 |
Phase 4: Invitations + UI
| # | Завдання | Підстава |
|---|---|---|
| 11 | Endpoint /dashboard/invite-helper (генерація invitation) |
Шлях 3 |
| 12 | Endpoint /invite/{token} (прийом запрошення) |
Шлях 3 |
| 13 | Сторінка /dashboard/approval-queue для senior'а |
Self-register schвалення |
| 14 | Кнопка «🗄 Архівувати» у списку юзерів | Soft-delete |
Phase 5: Data approval (NEW в v1.6)
| # | Завдання | Підстава |
|---|---|---|
| 15 | Auto-set data_status='approved' для owner self-write |
Довіра owner'у |
| 16 | Auto-set data_status='pending' для helper/manager write |
Workflow |
| 17 | Сторінка «📋 Перевірити записи помічників» (owner) | Approval queue |
| 18 | Endpoint /dashboard/approve-visit/{id} (✓/✕ з коментарем) |
Approval action |
| 19 | Бейдж «N pending» на головній owner'а | UX |
| 20 | Сторінка «📊 Записи спілки» (senior, read-only audit) | Senior 2-й рівень |
| 21 | Бейдж «N своїх pending/rejected» для helper | Прозорість |
| 22 | Server-side валідація: межі numeric, sanity checks | Гейт-1 |
| 23 | BLUP-pipeline filter WHERE data_status='approved' |
Якість даних |
Phase 6: Phase-out + polish
| # | Завдання | Підстава |
|---|---|---|
| 24 | Прибрати з UI Telegram-логін, токени з бота | Phase out |
| 25 | Опціонально: 2FA для admin/senior | Безпека |
| 26 | Cron: cleanup expired invitations | Гігієна БД |
Принципи що тримаємо
- Telegram OUT — реєстрація і логін через email/сайт
- Спілка 001 — generic код, без hard-code інституцій
- Owner self-register, решта invitation-only — баланс friction/безпеки
- Магiclink-passwordless ≈ email+password з точки зору безпеки — обидва тримаються на захисті пошти
- Soft-delete як default — дані не губляться, тільки доступ
- Один beekeeper = одна пасіка (як owner) — CD-008 збережено
- Historical data integrity — FK ON DELETE SET NULL, recorded_by не зникає при видаленні юзера
- Data approval gate перед BLUP — owner схвалює дані помічників,
senior робить 2-й рівень audit, тільки
approvedйде в племінні цінності - Валідація на два рівні — platform автомат (межі, sanity) + людина (контекст: «чи не дурня?»)
Майбутнє (v2+)
- Multi-tenancy через
association_code(фільтр scope-у даних) - 2FA через TOTP (Google Authenticator)
- Інтеграція з ОRCID (для дослідницького контексту)
- API tokens для CLI/скриптів
- Audit log: хто кого зареєстрував/архівував, коли