190 lines
7.1 KiB
Markdown
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
|