diff --git a/docs/adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md b/docs/adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md new file mode 100644 index 0000000..a60b8e1 --- /dev/null +++ b/docs/adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md @@ -0,0 +1,80 @@ +# Хранилище, файлы и вход переезжают в PocketBase + +- **Дата:** 2026-08-11 +- **Источник:** [../research/pocketbase.md](../research/pocketbase.md) — записка + разведки `pocketbase-admin-fit` + +## Решение + +PocketBase заменяет SQLite с goqu и goose и берёт на себя три вещи разом: +состояние задач и метаданные, файлы записей своим полем и своей раскладкой на +диске, вход пользователей через Authelia своим провайдером OIDC. Панель +администратора работает по всем трём частям только в таком составе. + +## Почему + +Разведка мерила панель по трём частям, и порознь ни одна перевода не оправдывала. +Правку записей панель даёт целиком, а две остальные упираются в то, где лежат +данные. Цитата из источника, раздел про пользователей: + +> Панель показывает свою коллекцию пользователей и ничего больше. Отсюда +> следствие для целевого входа: **пользователи Authelia в панели не появятся, +> если вход делает само приложение**. + +И раздел про файлы: + +> Сегодняшняя раскладка `data/files` с именами-UUID панели не видна. Путь строкой +> она покажет строкой — прослушать и скачать запись по ней нельзя. Способа +> сослаться на файл, уже лежащий на диске мимо её каталога, нет. + +Там же то, что связывает файлы с сохранностью архива: + +> **Резервные копии накрывают ровно её каталог.** Файлы, оставленные снаружи, в +> них не попадут — то есть панель и встроенное резервное копирование покупаются +> одной и той же ценой. + +Отвергнуты два половинчатых пути, и оба по одной причине — они покупают перевод, +не покупая того, ради чего он затевался: + +> **Держать файлы на диске как сейчас, а в базе — путь строкой.** Отвергнуто: +> панель тогда не даёт по файлам ничего, и встроенные копии их не накрывают. +> Довод, ради которого перевод затевался, пропадает целиком. +> +> **Оставить вход у приложения, а PocketBase взять только хранилищем.** +> Отвергнуто: пользователей панель в этом случае не показывает вовсе, и одна из +> трёх частей вопроса остаётся без ответа навсегда, а не до какой-то задачи. + +Запись попадает в журнал по двум основаниям сразу. **Откат дорогой:** меняется +раскладка файлов на диске, а она в `../../CLAUDE.md` названа необратимой. +**Пересматривается прежнее решение:** вход через OIDC собирались делать в самом +приложении — так это записано в `../architecture.md`, «Открытые вопросы», и так +поставлена задача `oidc-login`. Парного статуса «заменено на» прежняя запись не +получает: своего ADR у неё нет, решение жило открытым вопросом архитектуры. + +## Последствия + +- `+` панель даёт владельцу править записи, видеть пользователей и слушать сами + файлы. Проверено на версии 0.39.10; в библиотечной сборке панель отдаётся по + адресу `/_/` того же порта. +- `+` база и записи съезжаются под один каталог, и копия сервера накрывает их + разом. Своё копирование по расписанию с выгрузкой в S3-совместимое хранилище у + PocketBase тоже есть, но копии сервер уже делает своими средствами — берём мы + встроенное или нет, здесь не решено. +- `+` требование CGO уходит: PocketBase ходит в SQLite через + `modernc.org/sqlite`, и пробник собрался при `CGO_ENABLED=0`. Свойство стека в + `../../CLAUDE.md` перестаёт быть верным. +- `−` раскладка `data/files` меняется необратимо: файл ложится в + `pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайных символов>` рядом с + файлом атрибутов. Момент перехода назначает человек; данные прежней базы не + переносятся по прежнему решению задачи `pocketbase-storage`. +- `−` вход перестаёт быть нашим: задача `oidc-login` переписывается с + собственной обработки ответа провайдера на настройку провайдера в PocketBase. + Что делать с сессией и где она живёт, решает уже не наш код. +- `−` появляется секрет, которого не было: пароль суперпользователя панели. Сама + PocketBase Authelia к панели не подпускает — ни OIDC, ни второй фактор у + коллекции суперпользователей включить не удалось. Своё ограничение по списку + адресов у неё есть, но им же можно запереть себя: сброса в наборе команд нет. +- `−` панель висит на том же порту, что и приложение, а порт опубликован в + интернет через обратный прокси. **Закрывает её контур:** тем же решением адрес + `/_/` закрывает Authelia на прокси, пропуская группу администраторов. Приложение + тут ни при чём, и задачи в беклоге у этого нет. diff --git a/docs/adr/README.md b/docs/adr/README.md index a9876e2..339033e 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -32,9 +32,8 @@ | Дата | Запись | Статус | | --- | --- | --- | +| 2026-08-11 | [Хранилище, файлы и вход переезжают в PocketBase](ADR-2026-08-11-pocketbase-storage-with-admin-panel.md) | | | 2026-08-11 | [Проверки не зовут внешних программ](ADR-2026-08-11-stub-adapters-in-tests.md) | | Решения, принятые до заведения канона 2026-08-10, источника в архиве изменений -не имеют — сочинять их задним числом правило запрещает. Ближайшие кандидаты -назовёт первое же изменение, которое тронет хранилище или вход: замена SQLite на -PocketBase и вход через OIDC оба подпадают под критерий «дорогой откат». +не имеют — сочинять их задним числом правило запрещает. diff --git a/docs/architecture.md b/docs/architecture.md index 786c0aa..a3ee149 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -119,11 +119,18 @@ ## Открытые вопросы -- **Хранилище.** Пробуем PocketBase взамен SQLite с goqu и goose. Не решено, чем - становится конвейер задач: таблицей PocketBase с тем же захватом или чем-то - другим. Данные не переносим — начинаем с чистого листа. -- **Учётные записи.** Вход через OIDC, провайдер — Authelia. Не решено, где - живёт сессия и как связываются пользователь Telegram и пользователь веба. +- **Хранилище.** PocketBase заменяет SQLite с goqu и goose, файлы переезжают в её + раскладку на диске — решено 2026-08-11, + [ADR](adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md), замер панели + в [research/pocketbase.md](research/pocketbase.md). Требование CGO этим + снимается. Не решено, чем становится конвейер задач: таблицей PocketBase с тем + же захватом или чем-то другим — разведка `job-queue-choice`. Данные не + переносим — начинаем с чистого листа. +- **Учётные записи.** Вход через OIDC, провайдер — Authelia, а ответ провайдера + обрабатывает PocketBase, а не наш код (тот же ADR). Не решено, где живёт сессия + и как связываются пользователь Telegram и пользователь веба. Панель + администратора при этом Authelia не закрывает: у неё свой пароль + суперпользователя. - **Приложение.** Экранов нет вовсе, есть только API. Решено делать SPA, устанавливаемое на телефон; фреймворк выбирает разведка `spa-framework-choice`, и до её итога @@ -149,6 +156,11 @@ `usage-accounting`. - **Срок хранения.** Записи и тексты решено хранить бессрочно (паспорт, 2026-08-11), а рост каталога `data/files` ничем не ограничен и не наблюдается. +- **Резервные копии.** Копии делает сервер своими средствами, и приложение о них + ничего не знает. После переезда на PocketBase не решено, хватит ли копировать + её каталог файлами, или приложению нужна команда выгрузки: база под нагрузкой + копируется файлом не всегда целой. Своё копирование по расписанию у PocketBase + есть — берём мы его или нет, тоже не решено. - **Формат для распознавания.** Конвертер отдаёт ogg/vorbis (`libvorbis`), а SpeechKit получает `ContainerAudio_OGG_OPUS`. Расхождение не разобрано: то ли сервис определяет содержимое сам, то ли часть записей теряется на этом. diff --git a/docs/passport.md b/docs/passport.md index 712f84c..8b896c0 100644 --- a/docs/passport.md +++ b/docs/passport.md @@ -58,7 +58,10 @@ - **Собственные модели.** Не обучаем и не держим у себя ни модель распознавания, ни языковую модель: и речь, и выводы из текста считает внешний сервис. - **Управление учётными записями.** Пользователей заводит и проверяет внешний - провайдер, свою регистрацию и свои пароли не делаем. + провайдер, свою регистрацию и свои пароли не делаем. Одно исключение появилось + 2026-08-11 вместе с решением про PocketBase: в её панель администратора + владелец входит своим паролем, потому что закрыть её провайдером она не + умеет. - **Живая расшифровка.** Работаем с готовой записью, поток в реальном времени не обрабатываем. - **Диктофон.** Запись звука делает телефон, а приложение принимает готовый @@ -106,6 +109,7 @@ которой пользуемся: она и задаёт потолок по длине записи и формату. - **Whisper и его серверные обёртки** — запасной путь, если внешний сервис перестанет устраивать по цене или по качеству русской речи. -- **PocketBase** — кандидат в хранилище взамен сегодняшнего SQLite. Источником - учётных записей его не рассматриваем: вход решено делать через OIDC у Authelia - ([architecture.md](architecture.md), «Открытые вопросы»). +- **PocketBase** — хранилище взамен сегодняшнего SQLite, решено 2026-08-11 + ([adr](adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md)). Учётные + записи оно хранит и получает от Authelia своим провайдером OIDC, но источником + их не становится: заводит и проверяет людей по-прежнему Authelia. diff --git a/docs/research/README.md b/docs/research/README.md index 9aabd87..742bd1e 100644 --- a/docs/research/README.md +++ b/docs/research/README.md @@ -8,8 +8,8 @@ ## Как снималось -Ничего не снималось. Записей нет: замеров на живом потоке не делали, поведение -внешних сервисов на границах не проверяли. +На живом потоке не снималось ничего: поведение внешних сервисов на границах не +проверяли. Единственная запись сделана на пустой базе в песочнице. Внешних источников, о которых разведка нужна, четыре — Telegram Bot API, Yandex SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, что стоит @@ -19,4 +19,6 @@ SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, чт ## Записи -Записей нет. +| Дата | Запись | О чём | +| --- | --- | --- | +| 2026-08-11 | [PocketBase: что даёт панель администратора](pocketbase.md) | Записи, пользователи и файлы в панели версии 0.39.10 | diff --git a/docs/research/pocketbase.md b/docs/research/pocketbase.md new file mode 100644 index 0000000..cd5b73a --- /dev/null +++ b/docs/research/pocketbase.md @@ -0,0 +1,108 @@ +# PocketBase: что даёт панель администратора + +Отвечает на вопрос разведки `pocketbase-admin-fit`: что панель показывает и +правит по трём частям — записи, пользователи, файлы, — и хватает ли этого, чтобы +держать перевод хранилища в планах. + +## Как снималось + +Версия **0.39.10**, выпуск от 2026-07-30 (`./pocketbase --version`). Смотрел на +пустой базе в каталоге вне репозитория, боевые данные не участвовали. Прогонов +было два: + +- **готовый бинарник** — `pocketbase serve --http=127.0.0.1:8099`, суперпользователь + заведён командой `pocketbase superuser create`. Возможности панели снимал её же + запросами (`/api/collections`, `/api/logs`, `/api/backups`, `/api/crons`, + `/api/settings`) и поиском по её собранному коду; +- **своя сборка**, где PocketBase подключён библиотекой к пустому приложению на + Go, — так, как предполагает задача `pocketbase-storage`. + +Оба прогона удалены вместе с песочницей. + +## Правка записей — работает целиком + +Панель показывает каждую коллекцию таблицей, отбирает записи своим языком +фильтров, сортирует, создаёт, правит и удаляет их по одной. Сверх таблицы в ней +есть выгрузка списка в CSV, журнал запросов с временем ответа и кодом, резервные +копии с загрузкой и восстановлением, список заданий планировщика. + +Групповой операции над отмеченными записями в панели нет: удаление идёт по +одной. Проверял поиском по её коду — строк вида «удалить отмеченное» в нём не +нашлось, тогда как «Export as CSV» и «Download JSON» нашлись. + +## Пользователи — только те, кого туда положат + +Панель показывает свою коллекцию пользователей и ничего больше. Отсюда следствие +для целевого входа: **пользователи Authelia в панели не появятся, если вход +делает само приложение**. Пустая база заводит шесть коллекций, из них одна +пользовательская (`users`) и пять служебных, включая `_externalAuths` — связь +записи с внешним провайдером. + +Второй путь есть, и он работает: **вход можно отдать самой PocketBase**. У +пользовательской коллекции настраивается провайдер `oidc` с произвольными +адресами; я включил его на адреса вида `https://auth.example.com/api/oidc/...`, +и клиент немедленно стал получать провайдера в списке способов входа. Тогда +учётные записи заводятся сами, и панель их видит. + +**В саму панель Authelia не пускает.** Вход суперпользователя — своя почта и свой +пароль: + +- включить `oidc` у коллекции суперпользователей не удалось: запрос принимается, + но возвращает коллекцию с выключенным `oauth2`; +- включить второй фактор у неё же не удалось тоже — ответ `403`. + +Ограничить панель списком адресов можно: настройка `superuserIPs` принимает +адреса и подсети. **Ею же можно запереть себя** — после того как я поставил туда +чужой адрес, все запросы суперпользователя, включая запрос на сброс настройки, +стали отвечать `403`. Команды сброса в наборе нет: он состоит из `migrate`, +`superuser`, `update` и `serve`. + +## Файлы — только свои + +Файл живёт полем записи, и раскладку на диске выбирает PocketBase: + +``` +pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайных символов>.ogg +pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайных символов>.ogg.attrs +``` + +Проверено загрузкой файла в 200 КБ: имя `sample.ogg` превратилось в +`sample_uztrv6wvz3.ogg`, рядом лёг файл атрибутов. + +Сегодняшняя раскладка `data/files` с именами-UUID панели не видна. Путь строкой +она покажет строкой — прослушать и скачать запись по ней нельзя. Способа сослаться +на файл, уже лежащий на диске мимо её каталога, нет. + +Поле помечается защищённым, и тогда файл не отдаётся по прямой ссылке: без токена +ответ `404`, с выданным файловым токеном — `200`. + +**Резервные копии накрывают ровно её каталог.** Файлы, оставленные снаружи, в них +не попадут — то есть панель и встроенное резервное копирование покупаются одной и +той же ценой. + +## Побочное: CGO уходит + +Библиотечная сборка встала при `CGO_ENABLED=0` — PocketBase ходит в SQLite через +`modernc.org/sqlite`, а не через `mattn/go-sqlite3`. Требование CGO записано +сегодня свойством стека в `../../CLAUDE.md`, и перевод его снимает. + +Бинарник пробника — 33 954 634 байта против 43 498 904 у сегодняшнего приложения +(`go build` без флагов). **Числа не сравнимы напрямую:** в пробнике нет ни бота, +ни клиента SpeechKit, ни клиента Object Storage. Что даст сборка после перевода, +не замерялось. + +Панель отдаётся по адресу `/_/` того же порта, что и остальное приложение, — и в +библиотечной сборке тоже: пустое приложение с одним своим обработчиком отвечало +на `/_/` кодом `200`. + +## Что отвергнуто и почему + +- **Держать файлы на диске как сейчас, а в базе — путь строкой.** Отвергнуто: + панель тогда не даёт по файлам ничего, и встроенные копии их не накрывают. + Довод, ради которого перевод затевался, пропадает целиком. +- **Оставить вход у приложения, а PocketBase взять только хранилищем.** + Отвергнуто: пользователей панель в этом случае не показывает вовсе, и одна из + трёх частей вопроса остаётся без ответа навсегда, а не до какой-то задачи. +- **Отказаться от перевода.** Отвергнуто человеком на чекпоинте 2026-08-11: + вместе с панелью отказ выбрасывал бы уход CGO и встроенное резервное + копирование, которых у сервиса-архива нет никаких. diff --git a/docs/security.md b/docs/security.md index 55bb177..96e2af6 100644 --- a/docs/security.md +++ b/docs/security.md @@ -17,6 +17,17 @@ сделало сервис архивом. Оба сдвига описаны ниже разделами «Куда уходит содержимое записи» и «Что вне модели». +**Третий сдвиг — панель администратора.** Решением от 2026-08-11 +([adr](adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md)) хранилищем +становится PocketBase, и вместе с ним на том же порту появляется панель по +адресу `/_/`: доступ ко всем записям, всем файлам и всем пользователям разом. +Порт опубликован в интернет через обратный прокси, а сама PocketBase вход в +панель через Authelia не пускает — у неё свой пароль суперпользователя. +**Закрывает панель контур, а не приложение:** решением владельца от 2026-08-11 +адрес `/_/` закрывает Authelia на обратном прокси, пропуская только группу +администраторов. Задачи в беклоге у этого нет — работа принадлежит выкладке, а +она вне модели («Что вне модели», строка про контур). + Отсюда главное следствие, из которого читается всё остальное: **`POST /api/audio` доступен кому угодно из интернета**. Отправитель не назван, не ограничен по числу запросов и не ограничен по размеру файла. @@ -93,6 +104,14 @@ Telegram отправителю. заодно сообщал бы, что запись у кого-то уже есть. - **Файлы фрагментов** (`long-audio-chunking`) ложатся рядом с исходным в тот же плоский каталог — раскладка `data/files` меняется, и это необратимо. +- **Раскладку выбирает PocketBase** (`pocketbase-storage`), и плоского каталога + не остаётся вовсе: файл ложится в + `pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайных символов>` рядом с + файлом атрибутов. Имя, данное отправителем, в путь при этом попадает — сегодня + от него берётся только расширение. Файл уходит не с диска напрямую, а по ссылке + вида `/api/files/<коллекция>/<запись>/<имя>`; закрытым он становится, только если + поле помечено защищённым, и тогда нужен отдельный файловый токен. Замер — + [research/pocketbase.md](research/pocketbase.md). - **Имя отправляемого документа** (`long-text-delivery`) собирается из идентификатора задачи: имя, данное пользователем, в него не попадает. @@ -126,6 +145,14 @@ Telegram отправителю. Откуда он берётся — из группы OIDC или из конфигурации — не решено (`admin-stats-screen`). +**Панель администратора в эту таблицу не входит и разграничению не подчиняется.** +Суперпользователь PocketBase видит все записи, все файлы и всех пользователей +мимо любого из четырёх механизмов, а пускает его свой пароль, а не Authelia. +Замер показал, что закрыть панель провайдером OIDC или вторым фактором нельзя: +обе настройки у коллекции суперпользователей отклоняются. Остаётся ограничение +по списку адресов (`superuserIPs`), и оно же запирает владельца, если список +задан неверно: сброса в наборе команд нет. + ## Что чувствительнее чего 1. **Содержимое записей и расшифровок.** Голосовые сообщения — личная переписка; @@ -153,6 +180,10 @@ Telegram отправителю. 5. **Статистика потребления** (`usage-accounting`). Текста записей не содержит, но говорит, кто и когда пользовался сервисом и сколько; страница расхода открыта только владельцу. +6. **Пароль суперпользователя панели** (`pocketbase-storage`). Открывает все + записи, все файлы и всех пользователей разом, то есть стоит вровень с самым + чувствительным из списка выше. Второй секрет после токенов пользователей, + который лежит **не в конфигурации**: его отпечаток хранит сама база. Тексты расшифровок в логи не пишутся — логируется длина текста и идентификаторы. **Имя файла, данное отправителем, пишется**: строка diff --git a/tasks/BACKLOG.md b/tasks/BACKLOG.md index 74b8675..5bf9935 100644 --- a/tasks/BACKLOG.md +++ b/tasks/BACKLOG.md @@ -25,8 +25,8 @@ - [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата. - [🔬 Админка PocketBase: данные, пользователи и файлы](items/pocketbase-admin-fit.md) — Переход на PocketBase решён ради его панели администратора, а что она даёт по данным, пользователям и файлам на диске — не проверено. - [🔬 Очередь задач: своя таблица или готовая библиотека](items/job-queue-choice.md) — Очередь написана вручную: захват двумя запросами без транзакции, протухание временем, опрос раз в секунду вхолостую тремя воркерами. -- [🧹 Перевести хранилище на встроенный PocketBase](items/pocketbase-storage.md) — Из трёх доводов за перевод остался один: учётные записи ушли к Authelia, страницу статистики рисует само приложение, а панель администратора не проверена. -- [✨ Пускать в приложение только после входа через OIDC](items/oidc-login.md) — HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору. +- [🧹 Перевести хранилище и файлы записей на встроенный PocketBase](items/pocketbase-storage.md) — Записи, метаданные и файлы лежат порознь, и владелец не видит их ничем: панель PocketBase покажет и то и другое, только если они переедут к ней. +- [✨ Пускать в приложение только после входа через OIDC](items/oidc-login.md) — HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору. Ответ провайдера разбирает PocketBase, а не наш код. - [✨ Привязать запись к владельцу и отдавать только свои](items/record-ownership.md) — У задачи и файла нет владельца, поэтому знание UUID задачи и есть право её читать. - [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны. - [✨ Свести приём и чтение записей к одному контракту для приложения](items/json-api-for-spa.md) — Сегодняшний API отвечает 404 на любую ошибку чтения и 500 на любую ошибку приёма: строить на нём экраны нечем. diff --git a/tasks/items/oidc-login.md b/tasks/items/oidc-login.md index 1319b88..a14b793 100644 --- a/tasks/items/oidc-login.md +++ b/tasks/items/oidc-login.md @@ -2,7 +2,7 @@ - **Тип:** feature - **Категория:** Очередь -- **Зачем:** HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору. +- **Зачем:** HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору. Ответ провайдера разбирает PocketBase, а не наш код. - **Теги:** goal:multi-user, question Двигает пункт 1 «Завершения» цели: неаутентифицированный запрос к записям не @@ -13,12 +13,22 @@ здесь ещё нет: после входа видно всё, что видно сейчас, — этим занимается `record-ownership`. +**Ответ провайдера разбирает PocketBase, а не наш код** — решено 2026-08-11 +([adr](../../docs/adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md)). +У её коллекции пользователей настраивается провайдер `oidc` с адресами Authelia, +и учётные записи заводятся сами; проверено на версии 0.39.10, +[docs/research/pocketbase.md](../../docs/research/pocketbase.md). Отсюда порядок: +задача идёт после `pocketbase-storage`, до неё настраивать нечего. + ## Затрагивает -- новые эндпоинты входа и выхода, обработка ответа провайдера; +- настройка провайдера `oidc` у коллекции пользователей PocketBase: адреса + Authelia, идентификатор клиента, секрет, соответствие полей учётной записи; +- эндпоинты входа и выхода, которые PocketBase приносит своими; - `POST /api/audio` и `GET /api/status/:id` — оба уходят за аутентификацию; - `GET /health` и `GET /metrics` — решить и записать, остаются ли открытыми; -- хранение сессии: таблица или подписанная кука; +- хранение сессии: её ведёт PocketBase, и решить надо, чем она предъявляется + приложению; - секция конфигурации под провайдера: адрес, идентификатор клиента, секрет; - `docs/security.md` — периметр меняется, и первая его строка перестаёт быть верной; @@ -42,7 +52,9 @@ Владельца у записи здесь не заводим и выборку не сужаем: после входа видно столько же, сколько сейчас. Инвариант «бот отвечает только тем, кто в белом -списке» не трогаем — он живёт до `telegram-account-link`. +списке» не трогаем — он живёт до `telegram-account-link`. Панель администратора +тоже не трогаем: наружу её закрывает Authelia на обратном прокси, а это работа +выкладки. ## Вопросы diff --git a/tasks/items/pocketbase-storage.md b/tasks/items/pocketbase-storage.md index 13558be..125546c 100644 --- a/tasks/items/pocketbase-storage.md +++ b/tasks/items/pocketbase-storage.md @@ -1,18 +1,21 @@ -# 🧹 Перевести хранилище на встроенный PocketBase +# 🧹 Перевести хранилище и файлы записей на встроенный PocketBase - **Тип:** chore - **Категория:** Очередь -- **Зачем:** Из трёх доводов за перевод остался один: учётные записи ушли к Authelia, страницу статистики рисует само приложение, а панель администратора не проверена. +- **Зачем:** Записи, метаданные и файлы лежат порознь, и владелец не видит их ничем: панель PocketBase покажет и то и другое, только если они переедут к ней. PocketBase встраивается библиотекой в тот же бинарник и приносит хранилище, -учётные записи и панель администратора разом. Наблюдаемое поведение сервиса -после перевода не меняется: те же два входа, тот же конвейер, тот же текст на -выходе. +учётные записи и панель администратора разом. Конвейер и оба входа работают +по-прежнему, а снаружи прибавляется панель по адресу `/_/`. -Из трёх доводов 2026-08-11 осталось полтора: учётные записи заводит Authelia по -OIDC, страницу статистики рисует само приложение. Довод про панель -администратора проверяет разведка `pocketbase-admin-fit`, и её ответ решает, -берётся эта задача или уходит в `REJECTED.md`. +Разведка `pocketbase-admin-fit` довод проверила, и 2026-08-11 принято решение +[перевести хранилище вместе с файлами](../../docs/adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md): +панель показывает файлы и пользователей только тех, что лежат у неё, а половина +перевода довода не окупает. Замер — [docs/research/pocketbase.md](../../docs/research/pocketbase.md). + +**Файлы переезжают в раскладку PocketBase**, а плоского каталога `data/files` с +именами-UUID не остаётся. Это необратимо, и момент перехода назначает человек. +Вход в этой задаче не трогаем: его переводит `oidc-login`. Данные не переносим — база заводится с чистого листа, и это решение принято сознательно. @@ -24,10 +27,20 @@ OIDC, страницу статистики рисует само приложе - `internal/adapter/repo/sqlite` целиком, включая захват задачи через `FindAndAcquire`; - `internal/contract`, интерфейсы `FileRepository` и `TranscriptJobRepository`; -- ключ конфигурации `[database] path` и раскладка каталога `data/`; +- ключ конфигурации `[database] path`, ключ `[storage] path` и раскладка + каталога `data/`; +- **формат файла на диске:** запись переезжает в поле коллекции, путь становится + `pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайных символов>` рядом с + файлом атрибутов. Имя, данное отправителем, впервые попадает в путь целиком; +- отдача файла: вместо чтения с диска — ссылка `/api/files/...`, и решение, + помечать ли поле защищённым; - сборка образа: PocketBase тянет свой набор зависимостей, а `mattn/go-sqlite3` - с его требованием CGO может уйти; -- `docs/database.md` — схема, представление данных и таблица настроек. + с его требованием CGO уходит — библиотека ходит в SQLite через + `modernc.org/sqlite`; +- пароль суперпользователя панели: где заводится и как попадает на сервер; +- `docs/database.md` — схема, представление данных и таблица настроек; +- `CLAUDE.md` — строка стека про CGO и раздел про запреты с путями; +- `docs/security.md` — раздел «Из чего строятся пути и ключи». ## Критерии приёмки @@ -39,6 +52,9 @@ OIDC, страницу статистики рисует само приложе получает запись. - Задача, брошенная на середине, достаётся снова по истечении срока захвата. Оракул — тест с проставленным задним числом `acquire_time`. +- Принятая запись видна в панели строкой и скачивается из неё файлом. Оракул — + прогон записи через `POST /api/audio` на пустом каталоге, затем запрос + `/api/files/...` за тем же файлом: длина совпадает с загруженной. - `docs/database.md` описывает новую схему, а старые упоминания goose и goqu из документов канона убраны. Оракул — `task gate`, шаг `docs.py check`. @@ -46,4 +62,5 @@ OIDC, страницу статистики рисует само приложе Данные прежней базы не переносим и не пытаемся сохранить; выкладку не запускаем; смена формата хранения на сервере необратима, и момент перехода назначает -человек. +человек. Вход не трогаем — он на `oidc-login`. Панель наружу закрывает Authelia +на обратном прокси: это работа выкладки, а не приложения.