multiple groups per user
This commit is contained in:
@@ -83,8 +83,9 @@ groups:
|
||||
name: Family
|
||||
users:
|
||||
- email: alice@example.com # уникален глобально, хранится в нижнем регистре
|
||||
groupId: 3f1c…
|
||||
role: superadmin # user | admin | superadmin
|
||||
superadmin: true # глобальный флаг
|
||||
memberships: # одна или несколько групп, в каждой своя роль
|
||||
- { groupId: 3f1c…, role: admin } # user | admin
|
||||
keys:
|
||||
- id: 9a0b… # UUID
|
||||
label: Alice's phone # опционально
|
||||
@@ -121,13 +122,13 @@ cards:
|
||||
|
||||
### 3.3. Пользователи, группы, роли
|
||||
|
||||
- Пользователь состоит ровно в одной группе.
|
||||
- Пользователь состоит в одной или нескольких группах (но не в нуле), в каждой — со своей ролью `user` или `admin`. `superadmin` — глобальный флаг: суперадмин — администратор всех групп. Записи старого формата (`groupId` + `role`) читаются как членство в одной группе и сохраняются в новом формате при первой записи.
|
||||
- `admin` — администратор только своей группы. `superadmin` — администратор всех групп.
|
||||
- Карты общие для всех пользователей группы.
|
||||
- Группу, в которой есть хотя бы один суперадмин, удалить нельзя. При удалении группы удаляются все её пользователи, их ключи и все карты с картинками.
|
||||
- Нельзя удалить самого себя и нельзя понизить свою роль. Нельзя удалить или понизить последнего суперадмина.
|
||||
- `admin` может создавать, удалять и менять роль (`user`/`admin`) пользователей своей группы. Назначать `superadmin`, изменять суперадминов и переносить пользователей между группами может только `superadmin`.
|
||||
- Новые ключи генерирует только админ (для пользователей своей группы) или суперадмин.
|
||||
- Группу нельзя удалить, пока она единственная для какого-либо пользователя (сервер перечисляет таких пользователей). При удалении группы удаляются её карты с картинками и членства в ней; пользователи остаются в своих других группах.
|
||||
- Нельзя удалить себя из группы, менять свою роль и свой флаг superadmin. Нельзя снять флаг с последнего суперадмина или удалить его.
|
||||
- Админ группы добавляет в неё пользователей (новый email создаёт пользователя, существующий — добавляет ему членство), меняет их роль в этой группе и убирает их из неё; если группа была у пользователя единственной, пользователь удаляется вместе с ключами. Назначать и снимать superadmin и изменять суперадминов может только суперадмин.
|
||||
- Новые ключи генерирует только админ (для участников своей группы) или суперадмин. Ключ принадлежит пользователю и даёт приложению доступ ко всем его группам.
|
||||
|
||||
### 3.4. Ключи доступа
|
||||
|
||||
@@ -139,7 +140,7 @@ cards:
|
||||
### 3.5. Аутентификация административного UI
|
||||
|
||||
- Пользователь определяется по заголовку `Remote-Email` (без учёта регистра). Нет заголовка или пользователь не найден в `users.yaml` — `401`. Недостаточно прав — `403`.
|
||||
- Текущая группа: для `user` и `admin` всегда своя группа. Для `superadmin` — значение cookie `cardy_group` (если его нет или группа не существует — своя группа); UI суперадмина позволяет выбрать другую группу, что записывает cookie.
|
||||
- Текущая группа — значение cookie `cardy_group`, если пользователь в ней состоит (суперадмин — любая существующая группа); иначе первая группа пользователя. Переключатель в шапке есть у всех, у кого больше одной группы (у суперадмина — все группы). Роль и права в UI определяются ролью в текущей группе.
|
||||
- Если задан `ADMIN_PROXY_SECRET`, без правильного заголовка `X-Cardy-Proxy-Secret` — `401`.
|
||||
- Изменяющие запросы (`POST`/`PUT`/`PATCH`/`DELETE`) к админскому API требуют заголовка `X-Cardy-Admin: 1` (защита от CSRF).
|
||||
|
||||
@@ -163,9 +164,9 @@ cards:
|
||||
| `GET /my-keys` | все | свои ключи (расшифрованные) и deep link |
|
||||
| `GET /groups` | superadmin | список групп |
|
||||
| `POST /groups`, `PATCH /groups/:id`, `DELETE /groups/:id` | superadmin | CRUD групп |
|
||||
| `PUT /current-group` | superadmin | выбор текущей группы (cookie) |
|
||||
| `GET /users` | admin | пользователи текущей группы с ключами |
|
||||
| `POST /users`, `PATCH /users/:email`, `DELETE /users/:email` | admin | CRUD пользователей |
|
||||
| `PUT /current-group` | все (своя группа), superadmin (любая) | выбор текущей группы (cookie) |
|
||||
| `GET /users` | admin | участники текущей группы: роль в ней, флаг superadmin, все их группы, ключи |
|
||||
| `POST /users` `{email, role}` | admin | добавить в текущую группу (новый email создаёт пользователя) |
|
||||
| `POST /users/:email/keys`, `DELETE /users/:email/keys/:keyId` | admin | генерация и удаление ключей |
|
||||
| `GET /cards` | admin | все карты текущей группы, включая удалённые |
|
||||
| `POST /cards`, `PATCH /cards/:id` | admin | создание и изменение (JSON; `deleted: false` восстанавливает) |
|
||||
@@ -176,11 +177,11 @@ cards:
|
||||
|
||||
### 3.8. Клиентский API (публично `/api`, на сервере — от корня)
|
||||
|
||||
Все запросы с `Authorization: Bearer <ключ>`; неверный или отсутствующий ключ — `401`. Группа определяется по владельцу ключа. Удалённые карты для клиентского API не существуют (`404`). CORS разрешён для любого origin (нужно для Capacitor, origin `https://localhost`); cookie не используются.
|
||||
Все запросы с `Authorization: Bearer <ключ>`; неверный или отсутствующий ключ — `401`. Ключ даёт доступ ко всем группам владельца: маршруты карт принимают группу — `/api/groups/:groupId/cards…` (чужая группа — `404`); прежние маршруты без группы (`/api/cards…`) работают с первой группой владельца — для приложений, выпущенных до поддержки нескольких групп. Удалённые карты для клиентского API не существуют (`404`). CORS разрешён для любого origin (нужно для Capacitor, origin `https://localhost`); cookie не используются.
|
||||
|
||||
| Метод и путь | Описание |
|
||||
|---|---|
|
||||
| `GET /api/auth` | проверка ключа; возвращает email и группу |
|
||||
| `GET /api/auth` | проверка ключа; возвращает email, `groups` (все группы владельца) и `group` (первая) |
|
||||
| `GET /api/cards` | все неудалённые карты группы, без пагинации |
|
||||
| `POST /api/cards` | создать карту (multipart: поля `name`, `color`, `code`, `codeType`, опционально `comment`, `tags` (строка через запятую) и файл `logo`); возвращает карту с ID |
|
||||
| `PUT /api/cards/:id` | изменить карту (multipart: любые из тех же полей — не переданные не меняются, пустой `comment` очищает комментарий, пустые `tags` — теги; опционально файл `logo`, опционально `removeLogo=true`) |
|
||||
@@ -203,7 +204,7 @@ React + Vite + TS; PWA и Android-приложение через Capacitor. PWA
|
||||
- **Экран нового входа**: поле адреса API (в PWA по умолчанию — `<текущий origin>/api`), сканирование QR-кода ключа камерой (из QR берутся и адрес, и ключ), ручной ввод ключа. Обязательна проверка ключа на сервере (`GET <адрес API>/auth`). Экран можно вызвать и при сохранённом ключе — старый ключ удаляется, хранится только последний; вместе с ним удаляются все локальные данные (карты, картинки, PIN), после входа задаётся новый PIN и карты скачиваются автоматически.
|
||||
- **Навигация** хранится в истории браузера: каждый переход между экранами — запись в истории (с хэшем вида `#/card/<id>`), поэтому кнопка «назад» в тулбаре, кнопка «назад» браузера и системная кнопка Android работают одинаково; «вперёд» тоже работает. После перезагрузки PWA (и ввода PIN) открывается тот же экран. Ключ из ссылки входа в историю и адресную строку не попадает.
|
||||
- **Экран PIN-кода** (4 цифры): задаётся при смене ключа, сверяется при каждом открытии приложения. Защиты от перебора нет. Сменить PIN можно только через повторный вход («Forgot PIN? Sign in again» на экране PIN; отмена входа возвращает к экрану PIN). «Открытием» считается и возврат в приложение после 30 секунд в фоне.
|
||||
- **Экран карт**: список локально хранящихся карт (цветные плашки с лого или, если лого нет, с аббревиатурой названия на цветном фоне), сортировка по давности использования, поиск по подстроке (от 1 символа, без учёта регистра) в названии, тегах и комментарии, кнопки «обновить» и «добавить».
|
||||
- **Группы**: приложение хранит список групп пользователя и текущую группу; карты, даты использования и версии картинок хранятся отдельно для каждой группы. Если групп больше одной, заголовок экрана карт — название текущей группы, нажатие открывает выбор группы; группа, карты которой ещё не скачивались на устройство, при выборе сразу обновляется. «Обновить» заодно обновляет список групп (данные групп, в которых пользователь больше не состоит, удаляются). Сессии старых версий приложения (без групп) при первом обновлении получают список групп, а их локальные карты переходят в первую группу. (цветные плашки с лого или, если лого нет, с аббревиатурой названия на цветном фоне), сортировка по давности использования, поиск по подстроке (от 1 символа, без учёта регистра) в названии, тегах и комментарии, кнопки «обновить» и «добавить».
|
||||
- **«Обновить»**: скачивает список карт с сервера; при успехе заменяет локальный список (карты, пропавшие с сервера, удаляются локально вместе с картинками; даты использования сохраняются по ID карты). Затем для карт, у которых изменился `imagesVersion`, асинхронно скачиваются лого и картинки — не больше 5 запросов одновременно. Докачиваются и файлы, которых нет локально (например, после сбоя); версия картинок карты запоминается только когда скачаны все её файлы.
|
||||
- **Экран карты**: название, код (отрисовка `bwip-js`), теги, комментарий (с переносами строк), локальные картинки. На Android яркость окна поднимается до максимума (собственный плагин `ScreenBrightness` в `android/`) и возвращается к системной при уходе с экрана. Открытие экрана считается использованием карты (сохраняются дата и время). Кнопки «редактировать» и «удалить» (с подтверждением).
|
||||
- **Экран добавления и редактирования**: название, комментарий (многострочное поле), теги (через запятую), цвет (по умолчанию случайный из палитры цветов, контрастных с белым текстом), лого, картинки, код и тип кода — вручную или распознаванием с камеры или из загруженной картинки (`barcode-detector` на `zxing-wasm` и в PWA, и в Android WebView; wasm-модуль входит в сборку). После съёмки или выбора каждое изображение открывается на экране кадрирования: рамку можно двигать и менять за углы (для лого рамка квадратная, для фото свободная и по умолчанию охватывает всё изображение), изображение можно повернуть на 90°; «Cancel» пропускает изображение, несколько фото из галереи кадрируются по очереди; системная кнопка «назад» Android закрывает кадрирование (и сканер камеры), не уходя с экрана редактирования. Лого и картинки можно снять камерой (кнопка «Camera», поле выбора файла с `capture`) или выбрать из галереи («Gallery», для фото — сразу несколько). Лого и картинки перед отправкой пережимаются в WebP до 1000px по большей стороне, EXIF удаляется.
|
||||
|
||||
Reference in New Issue
Block a user