Добавил документ для разработчиков (про сессии но не закончил) и исправил мекую ошибку с несопостовлениеминдексов
This commit is contained in:
AidarKC 2026-03-27 14:44:01 +03:00
parent dabda362e6
commit 6d3719ba71
16 changed files with 522 additions and 4579 deletions

5
.gitignore vendored
View File

@ -1,3 +1,8 @@
## папки с данными создавайемыми при работе сервера
data/
logs/
logs
.gradle .gradle
build/ build/
!gradle/wrapper/gradle-wrapper.jar !gradle/wrapper/gradle-wrapper.jar

View File

@ -4,6 +4,9 @@
## Список документов ## Список документов
0. **API/01_Auth_and_Sessions_API.md**
API-глава для разработчиков: транспортный JSON-конверт, форматы запросов/ответов, создание и вход в сессию, `session_key`, `storagePwd`, подписи и совместимость версий.
1. **01_Connection_and_Sessions.md** 1. **01_Connection_and_Sessions.md**
Процесс подключения к WebSocket, авторизация (двухшаговая), создание сессии, вход в существующую сессию, просмотр и закрытие сессий. Процесс подключения к WebSocket, авторизация (двухшаговая), создание сессии, вход в существующую сессию, просмотр и закрытие сессий.

View File

@ -94,10 +94,13 @@
- Есть счетчики `message_stats` (likes/replies/edits) на уровне БД. - Есть счетчики `message_stats` (likes/replies/edits) на уровне БД.
- Есть `connections_state` как «текущее состояние», собранное триггерами. - Есть `connections_state` как «текущее состояние», собранное триггерами.
## 6) Важный текущий риск ## 6) Статус унификации subType
В проекте есть **несовпадение констант subType между модулями** (`shine-server-blockchain` и `shine-server-db`): Схема `subType` должна быть единой во всех модулях проекта:
- в blockchain-модуле `TEXT_POST=10, REPLY=20...`
- в db-модуле местами используются старые `TEXT_NEW=1, REPLY=2...`
Это нужно унифицировать, иначе часть триггеров/DAO будет работать некорректно. - `TEXT_POST = 10`
- `TEXT_EDIT_POST = 11`
- `TEXT_REPLY = 20`
- `TEXT_EDIT_REPLY = 21`
Это правило должно одинаково использоваться в `shine-server-blockchain`, `shine-server-db`, SQL-триггерах и DAO.

View File

@ -2,35 +2,31 @@
## Критичные вопросы ## Критичные вопросы
1. **Единая нумерация subType** 1. **Что считаем “сообщением канала” для counters**
- Подтверждаем ли окончательно новую схему (`POST=10, REPLY=20...`) во всех модулях (`db`, `triggers`, `dao`)?
2. **Что считаем “сообщением канала” для counters**
- Только `TEXT_POST`? - Только `TEXT_POST`?
- Или ещё `TEXT_EDIT_POST` и/или `REPLY` в этом же line? - Или ещё `TEXT_EDIT_POST` и/или `REPLY` в этом же line?
3. **Что такое “прочитано”** 2. **Что такое “прочитано”**
- По `this_line_number`? - По `this_line_number`?
- По `block_number`? - По `block_number`?
- По времени? - По времени?
4. **Личные и публичные каналы** 3. **Личные и публичные каналы**
- Явно вводим `channelType` в API? - Явно вводим `channelType` в API?
- Нужны ли отдельные private/dm каналы в MVP? - Нужны ли отдельные private/dm каналы в MVP?
5. **Уведомления (лайк/reply/follow/friend)** 4. **Уведомления (лайк/reply/follow/friend)**
- Делаем сначала виртуальный канал (query-time), потом материализацию? - Делаем сначала виртуальный канал (query-time), потом материализацию?
## Технический TODO (рекомендуемый порядок) ## Технический TODO (рекомендуемый порядок)
1. Унифицировать `MsgSubType` между модулями. 1. Добавить DAO для выборки каналов с counters.
2. Добавить DAO для выборки каналов с counters. 2. Добавить read-api handlers (3-4 операции, описанные в 04 документе).
3. Добавить read-api handlers (3-4 операции, описанные в 04 документе). 3. Добавить integration tests:
4. Добавить integration tests:
- подписка -> counters; - подписка -> counters;
- read progress -> newMessages; - read progress -> newMessages;
- thread graph на 100+ ответов. - thread graph на 100+ ответов.
5. Добавить индекс(ы) под новые query-паттерны (по `line_code`, `to_*`, `msg_type/subtype`). 4. Добавить индекс(ы) под новые query-паттерны (по `line_code`, `to_*`, `msg_type/subtype`).
## Дополнительные идеи ## Дополнительные идеи

View File

@ -0,0 +1,471 @@
# API для разработчиков: Авторизация и сессии
## Статус документа
Это **первая глава API-спецификации для клиентов**.
Документ фиксирует:
- единый JSON-формат запросов и ответов по WebSocket;
- роли `device key`, `session_key` и `storagePwd`;
- целевой формат подписываемых строк для авторизации;
- совместимость между текущей реализацией сервера и предлагаемым расширением.
---
## 1. Транспортный конверт
Все клиентские вызовы идут через WebSocket в общем JSON-конверте:
```json
{
"op": "OperationName",
"requestId": "req-001",
"payload": {
}
}
```
### Поля запроса
- `op` — имя операции.
- `requestId` — уникальный идентификатор запроса на стороне клиента.
- `payload` — объект с параметрами операции.
### Базовый формат ответа
Успешный ответ:
```json
{
"requestId": "req-001",
"status": 200,
"payload": {
}
}
```
Ответ с ошибкой:
```json
{
"requestId": "req-001",
"status": 400,
"error": "BAD_REQUEST",
"message": "Human readable description"
}
```
### Общие правила
- Все строки подписи и challenge собираются в UTF-8.
- Временные метки передаются в `timeMs` как Unix time в миллисекундах.
- Бинарные поля передаются как Base64-строки.
- `requestId` должен возвращаться сервером без изменений.
---
## 2. Роли ключей и секретов
### `device key`
Постоянный ключ устройства или аккаунта, которым клиент подтверждает право создать новую сессию.
Используется для:
- `CreateAuthSession`
### `session_key`
Клиент **сам создаёт** отдельный ключ сессии и передаёт на сервер только публичную часть.
Этот ключ используется для:
- `SessionLogin`
- последующих перевходов в уже созданную сессию
### `storagePwd`
`storagePwd` тоже **генерируется и передаётся клиентом** при создании сессии.
Сервер:
- сохраняет это значение в составе активной сессии;
- возвращает его клиенту после успешного `SessionLogin`.
Это нужно, чтобы клиент мог восстановить доступ к локально/серверно зашифрованному хранилищу сессии.
---
## 3. Формат `session_key` с префиксом алгоритма
Чтобы поддерживать разные аппаратные и программные типы ключей, `session_key` рекомендуется хранить и передавать не как "просто base64", а как строку с явным префиксом алгоритма:
```text
<algorithm>/<public-key-data>
```
Примеры:
```text
ed25519/MCowBQYDK2VwAyEA2I7...
secp256r1/BBD9LVa8gk9...
rsa2048/MIIBIjANBgkqh...
```
### Зачем это нужно
- у разных устройств разный набор аппаратно поддерживаемых ключей;
- серверу проще понимать, какой верификатор использовать;
- формат можно расширять без миграции всей таблицы сессий.
### Рекомендация по полю API
Во внешнем API лучше использовать поле:
```json
{
"sessionKey": "ed25519/BASE64_PUBLIC_KEY"
}
```
Если сервер внутри пока хранит старое поле `sessionPubKeyB64`, допускается переходный слой, который:
- принимает `sessionKey`;
- разбирает префикс алгоритма;
- сохраняет алгоритм и публичный ключ раздельно либо в одном поле.
---
## 4. Поток авторизации
Поддерживаются два базовых сценария:
1. Создание новой сессии.
2. Вход в существующую сессию.
---
## 5. Создание новой сессии
### Шаг 1. `AuthChallenge`
Клиент запрашивает nonce для логина.
Запрос:
```json
{
"op": "AuthChallenge",
"requestId": "auth-001",
"payload": {
"login": "alice"
}
}
```
Успешный ответ:
```json
{
"requestId": "auth-001",
"status": 200,
"payload": {
"login": "alice",
"authNonce": "8f2f0f71-0b1c-4ab2-8f5d-0bc5d6f6aa11",
"expiresInMs": 30000
}
}
```
Назначение:
- сервер убеждается, что пользователь существует;
- сервер связывает `authNonce` с текущим WebSocket-соединением;
- nonce одноразовый и живёт ограниченное время.
### Шаг 2. `CreateAuthSession`
Клиент:
- генерирует новый `session_key`;
- генерирует или выбирает `storagePwd`;
- подписывает строку создания сессии своим `device key`.
#### Целевой формат запроса
```json
{
"op": "CreateAuthSession",
"requestId": "create-001",
"payload": {
"login": "alice",
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
"storagePwd": "BASE64_OR_APP_SPECIFIC_SECRET",
"timeMs": 1774600000123,
"authNonce": "8f2f0f71-0b1c-4ab2-8f5d-0bc5d6f6aa11",
"signatureB64": "BASE64_SIGNATURE_BY_DEVICE_KEY",
"clientInfo": "Android 15; Pixel 9"
}
}
```
#### Целевая строка для подписи
Рекомендуемый формат:
```text
AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
```
Пример:
```text
AUTH_CREATE_SESSION:alice:ed25519/BASE64_PUBLIC_KEY:BASE64_OR_APP_SPECIFIC_SECRET:1774600000123:8f2f0f71-0b1c-4ab2-8f5d-0bc5d6f6aa11
```
### Почему `sessionKey` и `storagePwd` нужно включить в подпись
- сервер получает криптографическое подтверждение того, какие именно значения утвердил клиент;
- снижается риск подмены `session_key` между клиентом и сервером;
- `storagePwd` становится частью подтверждённого набора параметров создания сессии.
### Успешный ответ
```json
{
"requestId": "create-001",
"status": 200,
"payload": {
"sessionId": "sess_7c5e5c4b",
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
"createdAtMs": 1774600000201
}
}
```
---
## 6. Вход в существующую сессию
### Шаг 1. `SessionChallenge`
Запрос:
```json
{
"op": "SessionChallenge",
"requestId": "sch-001",
"payload": {
"sessionId": "sess_7c5e5c4b"
}
}
```
Успешный ответ:
```json
{
"requestId": "sch-001",
"status": 200,
"payload": {
"sessionId": "sess_7c5e5c4b",
"nonce": "0e5bb0f4-c7d8-4efb-b44d-bf31a6126c66",
"expiresInMs": 30000,
"sessionKeyAlgorithm": "ed25519"
}
}
```
### Шаг 2. `SessionLogin`
Клиент подписывает challenge приватной частью соответствующего `session_key`.
Запрос:
```json
{
"op": "SessionLogin",
"requestId": "slogin-001",
"payload": {
"sessionId": "sess_7c5e5c4b",
"timeMs": 1774600010456,
"signatureB64": "BASE64_SIGNATURE_BY_SESSION_KEY",
"clientInfo": "Android 15; Pixel 9"
}
}
```
Строка для подписи:
```text
SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
```
Пример:
```text
SESSION_LOGIN:sess_7c5e5c4b:1774600010456:0e5bb0f4-c7d8-4efb-b44d-bf31a6126c66
```
Успешный ответ:
```json
{
"requestId": "slogin-001",
"status": 200,
"payload": {
"sessionId": "sess_7c5e5c4b",
"storagePwd": "BASE64_OR_APP_SPECIFIC_SECRET",
"authenticatedAtMs": 1774600010500
}
}
```
---
## 7. Работа со списком сессий
### `ListSessions`
Доступно только после успешного `SessionLogin`.
Запрос:
```json
{
"op": "ListSessions",
"requestId": "list-001",
"payload": {
}
}
```
Успешный ответ:
```json
{
"requestId": "list-001",
"status": 200,
"payload": {
"sessions": [
{
"sessionId": "sess_7c5e5c4b",
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
"clientInfo": "Android 15; Pixel 9",
"lastAuthenticatedAtMs": 1774600010500,
"createdAtMs": 1774600000201,
"geo": "RU/Moscow"
}
]
}
}
```
### `CloseActiveSession`
Запрос:
```json
{
"op": "CloseActiveSession",
"requestId": "close-001",
"payload": {
"sessionId": "sess_7c5e5c4b"
}
}
```
Успешный ответ:
```json
{
"requestId": "close-001",
"status": 200,
"payload": {
"closed": true,
"sessionId": "sess_7c5e5c4b"
}
}
```
---
## 8. Ошибки и коды отказа
Минимально стоит стандартизовать такие ответы:
- `400 BAD_REQUEST` — не хватает поля или неверный формат.
- `401 UNAUTHORIZED` — challenge не был пройден или соединение не авторизовано.
- `403 INVALID_SIGNATURE` — подпись не прошла проверку.
- `404 SESSION_NOT_FOUND` — сессия не существует или уже закрыта.
- `409 NONCE_ALREADY_USED` — challenge уже использован.
- `410 CHALLENGE_EXPIRED` — nonce устарел.
- `422 UNSUPPORTED_KEY_ALGORITHM` — префикс `session_key` не поддерживается сервером.
- `429 TOO_MANY_ATTEMPTS` — лимит попыток исчерпан.
Пример:
```json
{
"requestId": "create-001",
"status": 422,
"error": "UNSUPPORTED_KEY_ALGORITHM",
"message": "sessionKey prefix is not supported"
}
```
---
## 9. Совместимость с текущей реализацией сервера
По текущему состоянию кода сервер уже использует схему:
- `AuthChallenge(login)`
- `CreateAuthSession(storagePwd, sessionPubKeyB64, timeMs, signatureB64, clientInfo)`
- `SessionChallenge(sessionId)`
- `SessionLogin(sessionId, timeMs, signatureB64, clientInfo)`
Текущая строка подписи для `CreateAuthSession` в коде:
```text
AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}
```
То есть **сейчас** `sessionPubKeyB64` и `storagePwd` ещё не входят в preimage подписи.
### Рекомендуемый путь миграции
1. Ввести новую версию контракта `CreateAuthSession`.
2. Добавить поле `sessionKey` вместо `sessionPubKeyB64`.
3. На сервере распознавать префикс алгоритма в `sessionKey`.
4. Перейти на подпись строки:
```text
AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
```
5. На переходный период поддерживать оба варианта:
- legacy: без `sessionKey` и `storagePwd` в подписи;
- vNext: с полным набором полей.
---
## 10. Практические требования к клиентам
- Клиент должен сам хранить приватную часть `session_key`.
- Приватная часть `device key` никогда не отправляется на сервер.
- `session_key` должен быть новым для каждой новой сессии.
- `storagePwd` должен генерироваться как криптографически стойкое значение.
- Клиент должен учитывать допустимый дрейф времени и синхронизацию часов.
- Клиент не должен повторно использовать старый `authNonce` или `nonce`.
---
## 11. Короткое резюме
- Да, клиент сам создаёт `session_key`.
- Да, клиент сам передаёт `storagePwd`.
- Для `session_key` имеет смысл ввести префикс алгоритма, например `ed25519/...`.
- Для `CreateAuthSession` рекомендуется подписывать не только `login`, `timeMs` и `authNonce`, но также `sessionKey` и `storagePwd`.
- Для разработчиков клиентов лучше сразу документировать API через полные JSON-примеры запросов и ответов.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -28,10 +28,10 @@ public final class DatabaseInitializer {
/* ===================== TEXT (msg_type=1) ===================== */ /* ===================== TEXT (msg_type=1) ===================== */
public static final short TEXT_NEW = 1; public static final short TEXT_POST = 10;
public static final short TEXT_REPLY = 2; public static final short TEXT_EDIT_POST = 11;
public static final short TEXT_REPOST = 3; public static final short TEXT_REPLY = 20;
public static final short TEXT_EDIT = 10; public static final short TEXT_EDIT_REPLY = 21;
/* ===================== REACTION (msg_type=2) ===================== */ /* ===================== REACTION (msg_type=2) ===================== */

View File

@ -306,12 +306,13 @@ public final class DatabaseTriggersInstaller {
} }
private static void createEditApplyTrigger(Statement st) throws SQLException { private static void createEditApplyTrigger(Statement st) throws SQLException {
int EDIT = (int) DatabaseInitializer.TEXT_EDIT; int EDIT_POST = (int) DatabaseInitializer.TEXT_EDIT_POST;
int EDIT_REPLY = (int) DatabaseInitializer.TEXT_EDIT_REPLY;
st.executeUpdate(""" st.executeUpdate("""
CREATE TRIGGER IF NOT EXISTS trg_blocks_edit_apply_ai CREATE TRIGGER IF NOT EXISTS trg_blocks_edit_apply_ai
AFTER INSERT ON blocks AFTER INSERT ON blocks
WHEN NEW.msg_type = 1 AND NEW.msg_sub_type = %d WHEN NEW.msg_type = 1 AND NEW.msg_sub_type IN (%d, %d)
BEGIN BEGIN
-- 1) помечаем исходный блок, что его "перекрыл" этот edit -- 1) помечаем исходный блок, что его "перекрыл" этот edit
UPDATE blocks UPDATE blocks
@ -346,6 +347,6 @@ public final class DatabaseTriggersInstaller {
AND NEW.to_block_number IS NOT NULL AND NEW.to_block_number IS NOT NULL
AND NEW.to_block_hash IS NOT NULL; AND NEW.to_block_hash IS NOT NULL;
END; END;
""".formatted(EDIT)); """.formatted(EDIT_POST, EDIT_REPLY));
} }
} }

View File

@ -18,17 +18,17 @@ public final class MsgSubType {
/* ===================== TEXT (msg_type=1) ===================== */ /* ===================== TEXT (msg_type=1) ===================== */
/** Новая публикация. */ /** POST — обычный пост в канале (в линии канала). */
public static final short TEXT_NEW = 1; public static final short TEXT_POST = 10;
/** Ответ (reply). */ /** EDIT_POST — редактирование исходного поста. */
public static final short TEXT_REPLY = 2; public static final short TEXT_EDIT_POST = 11;
/** Репост (repost). */ /** REPLY — ответ на сообщение. */
public static final short TEXT_REPOST = 3; public static final short TEXT_REPLY = 20;
/** Редактирование (edit). */ /** EDIT_REPLY — редактирование исходного ответа. */
public static final short TEXT_EDIT = 10; public static final short TEXT_EDIT_REPLY = 21;
/* ===================== REACTION (msg_type=2) ===================== */ /* ===================== REACTION (msg_type=2) ===================== */

View File

@ -13,7 +13,7 @@ import java.util.List;
* Возвращает по каждой активной подписке (FOLLOW) + "сам на себя": * Возвращает по каждой активной подписке (FOLLOW) + "сам на себя":
* - login цели (channelLogin) * - login цели (channelLogin)
* - blockchainName цели (channelBchName) * - blockchainName цели (channelBchName)
* - count публикаций (TEXT_NEW) * - count публикаций (TEXT_POST)
* - last publication: bytes оригинального блока (для timestamp) * - last publication: bytes оригинального блока (для timestamp)
* - last publication: bytes актуального блока (edit или orig) для текста превью * - last publication: bytes актуального блока (edit или orig) для текста превью
* *
@ -92,7 +92,7 @@ public final class SubscriptionsDAO {
/** /**
* Получить список подписок (активные FOLLOW) + "сам на себя" и по каждой: * Получить список подписок (активные FOLLOW) + "сам на себя" и по каждой:
* - count публикаций (TEXT_NEW) * - count публикаций (TEXT_POST)
* - последнюю публикацию (orig bytes) + её edit (если есть) * - последнюю публикацию (orig bytes) + её edit (если есть)
* *
* Поведение при 0 публикаций: * Поведение при 0 публикаций:
@ -207,11 +207,11 @@ public final class SubscriptionsDAO {
// pub_counts // pub_counts
ps.setInt(i++, MSG_TYPE_TEXT); ps.setInt(i++, MSG_TYPE_TEXT);
ps.setInt(i++, (int) MsgSubType.TEXT_NEW); ps.setInt(i++, (int) MsgSubType.TEXT_POST);
// last_pub // last_pub
ps.setInt(i++, MSG_TYPE_TEXT); ps.setInt(i++, MSG_TYPE_TEXT);
ps.setInt(i++, (int) MsgSubType.TEXT_NEW); ps.setInt(i++, (int) MsgSubType.TEXT_POST);
try (ResultSet rs = ps.executeQuery()) { try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) { while (rs.next()) {

View File

@ -16,7 +16,7 @@ package shine.db.entities;
* Плюс поля индексации: * Плюс поля индексации:
* - msg_type / msg_sub_type * - msg_type / msg_sub_type
* - to_* (если есть target) * - to_* (если есть target)
* - edited_by_block_number (для TEXT_EDIT) * - edited_by_block_number (для TEXT_EDIT_POST / TEXT_EDIT_REPLY)
*/ */
public class BlockEntry { public class BlockEntry {