Files
evo-sync/docs/plans/vk-connection.md
mguschin 379f781e1e feat: add connections dashboard with background health checks
- Add /connections page showing all integrations with online/offline status
- Add background health checker that polls Evotor API every 10 minutes
- Add is_online and last_checked_at fields to evotor_connections table
- Replace Evotor navbar link with unified Connections link
- Redirect connect/disconnect flows to /connections
- Add Alembic migration for new columns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 15:26:49 +03:00

190 lines
7.1 KiB
Markdown

# VK OAuth Connection Feature
## Context
EvoSync syncs product catalogs from Evotor to VK. Users already connect their Evotor account via OAuth. Now we need the same for VK — users authorize via VK OAuth, we store the access token, and show connection status on the connections dashboard.
## VK OAuth Flow (Web)
- **Authorize URL**: `https://oauth.vk.com/authorize`
- **Token URL**: `https://oauth.vk.com/access_token`
- **Verify endpoint**: `GET https://api.vk.com/method/users.get?access_token={token}&v=5.131`
- Error code 5 = token invalid/expired
- **Scopes**: `market groups offline` (offline = permanent token, no expiry)
- **Token response fields**: `access_token`, `user_id`, `expires_in` (0 if offline scope used)
With `offline` scope, tokens don't expire — no refresh logic needed. If a user revokes access on VK's side, the health checker will detect it.
## Plan
### 1. New Model — `VkConnection` in `web/models.py`
```python
class VkConnection(Base):
__tablename__ = "vk_connections"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), unique=True, nullable=False)
access_token = Column(Text, nullable=False)
vk_user_id = Column(String(50), nullable=True) # VK user ID from token response
first_name = Column(String(255), nullable=True) # VK profile first name
last_name = Column(String(255), nullable=True) # VK profile last name
is_online = Column(Boolean, default=False, server_default="0", nullable=False)
last_checked_at = Column(DateTime, nullable=True)
connected_at = Column(DateTime, server_default=func.now(), nullable=False)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
user = relationship("User", back_populates="vk_connection")
```
Add to `User` model:
```python
vk_connection = relationship("VkConnection", back_populates="user", uselist=False)
```
### 2. Alembic Migration
Generate migration for the new `vk_connections` table and the relationship.
### 3. Config — `web/config.py`
Add:
```python
VK_CLIENT_ID: str = ""
VK_CLIENT_SECRET: str = ""
VK_SCOPES: str = "market groups offline"
VK_API_VERSION: str = "5.131"
```
### 4. VK Route — `web/routes/vk.py` (new)
Follow the same pattern as `web/routes/evotor.py`:
**Constants:**
```python
VK_AUTHORIZE_URL = "https://oauth.vk.com/authorize"
VK_TOKEN_URL = "https://oauth.vk.com/access_token"
VK_API_URL = "https://api.vk.com/method"
```
**Endpoints:**
- `GET /vk` — Connection page. Shows connected state (VK profile name, user_id) or disconnected state with explanation and connect button.
- `GET /vk/connect` — Generate state token, save in session, redirect to:
```
https://oauth.vk.com/authorize?client_id={id}&response_type=code
&redirect_uri={BASE_URL}/vk/callback&scope={scopes}&state={state}
&display=page&v=5.131
```
- `GET /vk/callback` — OAuth callback:
1. Validate state from session
2. Exchange code for token via GET to `https://oauth.vk.com/access_token` with params: `client_id`, `client_secret`, `code`, `redirect_uri` (NOTE: VK uses GET, not POST, and params in query string, not body)
3. Response contains: `access_token`, `user_id`, `expires_in`
4. Fetch user profile via `users.get` to get first_name, last_name
5. Save/update `VkConnection` record with `is_online=True`, `last_checked_at=now()`
6. Redirect to `/connections`
- `POST /vk/disconnect` — Delete VkConnection record, redirect to `/vk`
### 5. VK Template — `web/templates/vk.html` (new)
Same structure as `evotor.html`:
**Connected state:**
- Status badge: "Подключено" (green)
- VK profile: first_name + last_name
- VK user ID (monospace)
- Connected timestamp
- Buttons: "Переподключить", "Отключить аккаунт ВКонтакте"
**Disconnected state:**
- Explanation text: "Подключите ваш аккаунт ВКонтакте, чтобы система могла автоматически синхронизировать каталог товаров из Эвотор в вашу группу ВКонтакте."
- Bullet points: redirect to VK for auth, auto-setup after confirmation, can disconnect anytime
- Button: "Подключить ВКонтакте"
**Error display:** same pattern as evotor.html (invalid_state, token_exchange, no_token)
**Back link:** "Вернуться к подключениям" → `/connections`
### 6. Register Route — `web/main.py`
```python
from web.routes import vk
app.include_router(vk.router)
```
### 7. Add to Connections Dashboard — `web/routes/connections.py`
Add VK entry to the connections list:
```python
vk_conn = db.query(VkConnection).filter(VkConnection.user_id == user.id).first()
connections.append({
"name": "ВКонтакте",
"icon": "bi-chat-dots", # or another suitable Bootstrap icon
"connected": vk_conn is not None,
"is_online": vk_conn.is_online if vk_conn else False,
"last_checked_at": vk_conn.last_checked_at if vk_conn else None,
"details": f"{vk_conn.first_name} {vk_conn.last_name}" if vk_conn and vk_conn.first_name else None,
"connect_url": "/vk",
"disconnect_url": "/vk/disconnect",
})
```
### 8. Background Health Check — `web/health_checker.py`
Add VK check alongside existing Evotor check:
```python
async def check_vk_connection(access_token: str) -> bool:
"""Call users.get to verify VK token is valid."""
async with httpx.AsyncClient() as client:
resp = await client.get(
"https://api.vk.com/method/users.get",
params={"access_token": access_token, "v": "5.131"},
timeout=10,
)
if resp.status_code != 200:
return False
data = resp.json()
# Error code 5 = invalid token
if "error" in data:
return False
return True
```
In `run_health_checks()`, add a loop over `VkConnection` rows with the same pattern as Evotor checks.
## Files Summary
| File | Action |
|------|--------|
| `web/models.py` | Modify — add `VkConnection` model + User relationship |
| `web/config.py` | Modify — add `VK_*` settings |
| `web/main.py` | Modify — register vk router |
| `web/routes/vk.py` | Create — OAuth flow (connect/callback/disconnect/page) |
| `web/routes/connections.py` | Modify — add VK to connections list |
| `web/health_checker.py` | Modify — add VK health check |
| `web/templates/vk.html` | Create — VK connection page |
| Alembic migration | Create — `vk_connections` table |
## Env Config Needed
```
VK_CLIENT_ID=your_vk_app_id
VK_CLIENT_SECRET=your_vk_app_secret
VK_SCOPES=market groups offline
```
## Verification
1. Run `alembic upgrade head`
2. Visit `/connections` — should show VK as disconnected (grey)
3. Click VK → "Подключить ВКонтакте" → redirects to VK auth
4. After VK auth → callback saves token → redirects to `/connections` → VK shows green
5. Visit `/vk` — shows connected state with VK profile info
6. Disconnect → VK returns to grey on connections page
7. Wait for health check cycle — verify `is_online` and `last_checked_at` update