Files
pchelpmaster/docs/data-model.md
T
kamaskandClaude Sonnet 5.5 115b5fb036 feat(users): configure users collection and validate email format
Migration sets access rules, password min 8, name max 100, password-only
auth, and a case-insensitive unique email index. A hook rejects non-ASCII
and malformed emails on every save. API tests cover registration, login,
and isolation between two users and a guest.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 18:24:33 +03:00

18 KiB
Raw Blame History

Модель данных (PocketBase)

Схема создаётся JS-миграциями в pocketbase/pb_migrations/ (одна миграция на изменение, пишется руками, не через админку). Кастомные маршруты и хуки — в pocketbase/pb_hooks/.

У всех коллекций, кроме users, есть поле owner — ссылка на владельца. Это основа изоляции: любые правила доступа начинаются с owner = @request.auth.id.

Поля id, created, updated (autodate) есть у всех коллекций и ниже не перечисляются.

users (auth-коллекция, уже существует в PocketBase)

Поле Тип Ограничения
email email обязательно, уникально без учёта регистра (индекс COLLATE NOCASE)
password password минимум 8 символов
name text необязательно, до 100 символов
verified bool системное; гость не может выставить true

Формат email дополнительно проверяет хук pb_hooks/users_email.pb.js при любом сохранении записи: только печатные символы ASCII без пробелов, минимум один символ до @, один после @, точка и минимум два символа после неё. Поэтому кириллические адреса отклоняются (400, ошибка в поле email), а индекс COLLATE NOCASE покрывает все допустимые адреса.

Способ входа — только email + пароль (OAuth2, OTP, MFA выключены).

Правило Значение Смысл
list / view id = @request.auth.id пользователь видит только себя
create "" (всем) открытая регистрация
update id = @request.auth.id меняет только себя
delete null (только суперпользователь) удаление аккаунта в MVP не делаем

Поле avatar, созданное PocketBase по умолчанию, можно оставить, в MVP оно не используется.

tags — метки

Метки заменяют и категории, и источники заявок: «Установка Windows», «YouDo», «Профи.ру», «Сарафан», «Гарантия» — это всё метки. Набор у каждого пользователя свой.

Поле Тип Ограничения
owner relation → users обязательно, одна, cascadeDelete: true
name text обязательно, 1–50 символов
color select одно из: gray, red, orange, yellow, green, teal, blue, purple, pink; обязательно

Индекс: уникальный (owner, name COLLATE NOCASE) — у одного пользователя нет двух меток с одинаковым названием без учёта регистра; у разных пользователей названия могут совпадать.

Цвет — ключ из фиксированной палитры, а не произвольный HEX: так UI гарантирует контраст AA в светлой и тёмной теме.

clients — клиенты

Поле Тип Ограничения
owner relation → users обязательно, cascadeDelete: true
name text обязательно, 1–200 символов (как клиента зовут или как он подписан на площадке)
phones json массив нормализованных номеров (["79991234567"]), до 5 штук, можно пустой
email email необязательно
contacts text свободный текст до 2000 символов: мессенджеры и прочие контакты
external_urls json необязательно; массив ссылок на профили клиента на любых площадках (YouDo, Профи.ру, Avito и др.), до 5 штук, каждая — валидный http(s) URL
address text необязательно, до 500 символов
note text внутренняя заметка, до 5000 символов

Нормализация телефона (делает фронтенд, функция в clients/domain): оставить только цифры; 8XXXXXXXXXX → 7XXXXXXXXXX; 10 цифр, начинающиеся на 9, → добавить 7. Остальные номера хранятся как есть (только цифры).

Удалить клиента, у которого есть заявки, нельзя (связь orders.client обязательная и без каскада): сначала удаляются или переносятся заявки.

orders — заявки

Поле Тип Ограничения
owner relation → users обязательно, cascadeDelete: true
client relation → clients обязательно, одна, без каскада
title text обязательно, 1–200 символов
description text описание проблемы со слов клиента, до 10000
status select обязательно: new, scheduled, in_progress, done, failed
scheduled_at date дата и время визита, необязательно
address text адрес выезда, до 500 (по умолчанию в форме подставляется адрес клиента)
price number целые рубли, ≥ 0, необязательно
result text что сделано, до 10000; виден по публичной ссылке
note text примечания мастера, до 10000; видны по публичной ссылке
review text отзыв клиента (например, с площадки), до 5000; виден по публичной ссылке
external_url url необязательно; ссылка на страницу заказа на любой площадке (YouDo, Профи.ру, Avito и др.)
tags relation → tags несколько, до 20, необязательно
attachments file несколько, до 20 файлов, до 50 МБ каждый, protected; изображения (image/jpeg, image/png, image/webp, image/heic) и аудио (audio/mpeg, audio/mp4, audio/x-m4a, audio/aac, audio/ogg, audio/wav, audio/webm, audio/amr); миниатюры 200x200
share_token text необязательно; если задан — 43 символа [A-Za-z0-9_-]; уникален среди непустых

Статусы и подписи в UI:

Значение Подпись
new Новая
scheduled Запланирована
in_progress В работе
done Выполнена
failed Не выполнена

Переходы между статусами не ограничены: мастер может выставить любой.

Индексы: owner, (owner, status), (owner, created), уникальный частичный share_token WHERE share_token != ''.

Соответствие полям YouDo (для справки)

YouDo Pchelpcrm
title, description title, description
price price
address, privateInfo address
customerName clients.name
customerUrl clients.external_urls
taskUrl external_url
category метка
review review
taskDate created / scheduled_at

Правила доступа для tags, clients, orders

Общий шаблон (для orders добавляются проверки связей):

Правило Значение
list / view owner = @request.auth.id
create @request.auth.id != "" && owner = @request.auth.id
update owner = @request.auth.id && (@request.body.owner:isset = false || @request.body.owner = @request.auth.id)
delete owner = @request.auth.id

Дополнительно для orders (create и update):

  • клиент принадлежит тому же пользователю: client.owner = @request.auth.id;
  • все метки принадлежат тому же пользователю (для пустого списка условие выполняется).

Если какое-то условие невозможно выразить правилом, его реализуют хуком в pocketbase/pb_hooks/. Решение принимается при реализации, поведение фиксируют тесты.

Как PocketBase отвечает на нарушения правил

API-тесты проверяют именно это поведение. Если фактический код в PocketBase 0.40 окажется другим, тесты фиксируют фактический, а главное требование неизменно: чужие данные никогда не попадают в ответ и не меняются.

Операция Правило не выполнено Правило null (не суперпользователь)
list 200, чужие записи отфильтрованы 403
view 404 403
create 400 403
update 404 403
delete 404 403

Публичный маршрут

GET /api/public/orders/{token} (хук в pocketbase/pb_hooks/), без авторизации.

  • Ищет заявку по непустому share_token, совпадающему точно.
  • Ответ 200 содержит только: title, description, result, note, review, status, scheduled_at, price, created, tags: [{ name, color }].
  • Никогда не содержит: id заявки, owner, данные клиента (имя, телефоны, контакты, адрес, ссылки на профили), address заявки, external_url, attachments, share_token, email мастера.
  • Нет заявки или токен пустой/неверного формата — 404 с одинаковым телом.
  • Заголовки: Cache-Control: no-store, X-Robots-Tag: noindex.

Ссылку создаёт владелец, записывая в share_token случайную строку (32 случайных байта в base64url, сгенерированные в браузере через crypto.getRandomValues). Отключить ссылку — записать пустую строку; перевыпустить — записать новую.