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

7.1 KiB

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

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:

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:

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:

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

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:

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:

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