# 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