# Синхронизация оффлайн лицензий с сервером

## Описание проблемы

Пользователь может активировать программу оффлайн (через лицензионный ключ) и работать длительное время в локальном SQLite режиме. Но когда он впервые подключается к серверу PostgreSQL, его учётной записи там может не быть.

## Решение

При первом подключении к PostgreSQL программа автоматически синхронизирует оффлайн лицензию с сервером.

## Процесс синхронизации

### Шаг 1: Проверка наличия оффлайн лицензии

```python
# В app.py при выборе PostgreSQL
license_key = self.config.get('license_key', None)
license_data_str = self.config.get('license_data', None)

if license_key and license_data_str:
    # Пользователь имеет оффлайн лицензию
    self._sync_offline_license_to_server()
```

### Шаг 2: Проверка существования пользователя

```python
username = license_data.get('username')
existing_user = self.users_repo.get_user_by_username(username)

if existing_user:
    # Пользователь уже есть - синхронизация не нужна
    return
```

### Шаг 3: Создание пользователя из лицензии

```python
# Создать пользователя без пароля
user_id = self.users_repo.create_user(
    username=username,
    email=None  # У оффлайн пользователей может не быть email
)

# Установить уровень доступа из лицензии
self.users_repo.update_user(
    user_id=user_id,
    access_level=license_data.get('access_level', 'full'),
    allowed_books=license_data.get('allowed_books')
)
```

### Шаг 4: Уведомление пользователя

```python
messagebox.showinfo(
    "User Synced",
    f"Ваш оффлайн аккаунт '{username}' синхронизирован с сервером!\n\n"
    "При первом входе вам нужно будет установить пароль."
)
```

## Сценарии использования

### Сценарий 1: Только оффлайн → Первое подключение к серверу

1. **День 1:** Пользователь активирует программу оффлайн (по почте получает ключ)
2. **День 1-30:** Работает в SQLite режиме без интернета
3. **День 31:** Подключается к интернету и выбирает PostgreSQL режим
4. **Автоматически:** Система создаёт его аккаунт на сервере из лицензии
5. **Результат:** Пользователь входит в первый раз, устанавливает пароль

### Сценарий 2: Сначала PostgreSQL → Потом оффлайн

1. **День 1:** Пользователь регистрируется на сервере
2. **День 10:** Уезжает в место без интернета
3. **День 10:** Получает оффлайн ключ по почте (с тем же username)
4. **День 10-20:** Работает в SQLite режиме
5. **День 21:** Возвращается, подключается к PostgreSQL
6. **Автоматически:** Система находит существующего пользователя, синхронизация не нужна

### Сценарий 3: Разные username оффлайн и онлайн

⚠️ **Проблема:** Если пользователь получил оффлайн ключ с username "user_offline", а потом зарегистрировался на сервере как "user_online", это будут **два разных аккаунта**.

**Решение:** Администратор должен выдавать оффлайн ключи с тем же username, что будет использоваться на сервере.

## Технические детали

### Когда вызывается синхронизация?

В `app.py` в методе `run()`:

```python
if data_source == "postgresql":
    # До показа LoginDialog
    self._sync_offline_license_to_server()
    
    # Показать LoginDialog
    login_dialog = LoginDialog(...)
```

### Что синхронизируется?

Из оффлайн лицензии на сервер переносятся:

| Поле | Описание |
|------|----------|
| `username` | Имя пользователя |
| `access_level` | Уровень доступа (guest/full/custom) |
| `allowed_books` | Список доступных книг (для custom) |
| `password_hash` | NULL (пользователь установит при первом входе) |
| `email` | NULL (не хранится в оффлайн лицензии) |

### Что НЕ синхронизируется?

- **Пароль** - пользователь должен установить при первом входе
- **Email** - не хранится в оффлайн лицензии
- **Machine ID** - используется только для валидации ключа
- **Срок действия** - только в оффлайн режиме

## Безопасность

### Предотвращение дублирования

```python
existing_user = self.users_repo.get_user_by_username(username)
if existing_user:
    # Не создавать дубликат
    return
```

### Предотвращение подделки

- Оффлайн ключ подписан SHA256 с секретным ключом
- Нельзя изменить `access_level` в ключе без знания SECRET_KEY
- Синхронизация использует данные из **проверенного** ключа

### Логирование

Все операции синхронизации логируются:

```python
LOGGER.info(f"Syncing offline user '{username}' to PostgreSQL")
LOGGER.info(f"Successfully synced offline user '{username}' to PostgreSQL (ID: {user_id})")
LOGGER.error(f"Failed to sync offline user '{username}' to PostgreSQL")
```

## Тестирование

Используйте скрипт `tests/test_offline_sync.py`:

```bash
python tests/test_offline_sync.py
```

Он проверит:
- Подключение к PostgreSQL
- Проверку существования пользователя
- Создание пользователя из лицензии
- Установку уровня доступа

## FAQ

**Q: Что если у пользователя изменился Machine ID?**  
A: Ему нужно запросить новый оффлайн ключ. Старый ключ не будет работать.

**Q: Можно ли синхронизировать несколько раз?**  
A: Да, но только при первом разе создастся пользователь. Последующие синхронизации будут игнорироваться (пользователь уже есть).

**Q: Что если администратор изменил access_level на сервере?**  
A: Серверная настройка имеет приоритет. Оффлайн лицензия используется только для **создания** пользователя.

**Q: Нужен ли интернет для синхронизации?**  
A: Да, синхронизация происходит при подключении к PostgreSQL серверу.

## Диаграмма потока

```
┌─────────────────────────────────────┐
│ Пользователь запускает программу    │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│ Выбирает PostgreSQL режим           │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│ Есть оффлайн лицензия?              │
└──────────┬──────────────────────────┘
           │
      Да   │   Нет
    ┌──────┴──────┐
    │             │
    ▼             ▼
┌───────────┐  ┌────────────────────┐
│ Синхрони- │  │ Показать           │
│ зация     │  │ LoginDialog        │
└─────┬─────┘  └────────────────────┘
      │
      ▼
┌─────────────────────────────────────┐
│ Пользователь существует на сервере? │
└──────────┬──────────────────────────┘
           │
      Нет  │  Да
    ┌──────┴─────┐
    │            │
    ▼            ▼
┌───────────┐ ┌──────────┐
│ Создать   │ │ Пропусти │
│ из лицен- │ │ ть       │
│ зии       │ │          │
└─────┬─────┘ └────┬─────┘
      │            │
      └────┬───────┘
           │
           ▼
┌─────────────────────────────────────┐
│ Показать LoginDialog                │
└─────────────────────────────────────┘
```

---

© 2025 BIgo Team
