import secrets from datetime import datetime import httpx from fastapi import APIRouter, Request, Depends from fastapi.responses import RedirectResponse from web.templates_env import templates from sqlalchemy.orm import Session from web.auth import get_current_user from web.config import settings from web.database import get_db from web.models import User, VkConnection router = APIRouter(prefix="/vk") 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" def _redirect_uri() -> str: return f"{settings.BASE_URL}/vk/callback" @router.get("") def vk_page( request: Request, db: Session = Depends(get_db), user: User | None = Depends(get_current_user), ): if not user: return RedirectResponse("/login", 303) connection = db.query(VkConnection).filter(VkConnection.user_id == user.id).first() error = request.query_params.get("error") return templates.TemplateResponse("vk.html", { "request": request, "user": user, "connection": connection, "error": error, }) @router.get("/connect") def vk_connect(request: Request, user: User | None = Depends(get_current_user)): if not user: return RedirectResponse("/login", 303) state = secrets.token_urlsafe(32) request.session["vk_oauth_state"] = state params = ( f"?client_id={settings.VK_CLIENT_ID}" f"&response_type=code" f"&redirect_uri={_redirect_uri()}" f"&scope={settings.VK_SCOPES.replace(' ', '%20')}" f"&state={state}" f"&display=page" f"&v={settings.VK_API_VERSION}" ) return RedirectResponse(VK_AUTHORIZE_URL + params, 302) @router.get("/callback") async def vk_callback( request: Request, db: Session = Depends(get_db), user: User | None = Depends(get_current_user), ): if not user: return RedirectResponse("/login", 303) code = request.query_params.get("code") state = request.query_params.get("state") saved_state = request.session.pop("vk_oauth_state", None) if not code or not state or state != saved_state: return RedirectResponse("/vk?error=invalid_state", 303) # Exchange code for token (VK uses GET with query params) try: async with httpx.AsyncClient() as client: token_response = await client.get( VK_TOKEN_URL, params={ "client_id": settings.VK_CLIENT_ID, "client_secret": settings.VK_CLIENT_SECRET, "code": code, "redirect_uri": _redirect_uri(), }, timeout=15, ) token_response.raise_for_status() token_data = token_response.json() except Exception: return RedirectResponse("/vk?error=token_exchange", 303) access_token = token_data.get("access_token") vk_user_id = str(token_data.get("user_id", "")) or None if not access_token: return RedirectResponse("/vk?error=no_token", 303) # Fetch VK profile info first_name = None last_name = None try: async with httpx.AsyncClient() as client: profile_response = await client.get( f"{VK_API_URL}/users.get", params={"access_token": access_token, "v": settings.VK_API_VERSION}, timeout=15, ) if profile_response.status_code == 200: profile_data = profile_response.json() items = profile_data.get("response", []) if items: first_name = items[0].get("first_name") last_name = items[0].get("last_name") except Exception: pass # Save or update connection connection = db.query(VkConnection).filter(VkConnection.user_id == user.id).first() if connection: connection.access_token = access_token connection.vk_user_id = vk_user_id connection.first_name = first_name connection.last_name = last_name connection.is_online = True connection.last_checked_at = datetime.utcnow() else: connection = VkConnection( user_id=user.id, access_token=access_token, vk_user_id=vk_user_id, first_name=first_name, last_name=last_name, is_online=True, last_checked_at=datetime.utcnow(), ) db.add(connection) db.commit() return RedirectResponse("/connections", 303) @router.post("/disconnect") async def vk_disconnect( request: Request, db: Session = Depends(get_db), user: User | None = Depends(get_current_user), ): if not user: return RedirectResponse("/login", 303) connection = db.query(VkConnection).filter(VkConnection.user_id == user.id).first() if connection: db.delete(connection) db.commit() return RedirectResponse("/connections", 303)