Files
evo-sync/web/routes/vk.py
mguschin 4d4d5b0118 Add Jivosite live chat widget support
Resolves #3 — widget is loaded on every page via base.html when
JIVOSITE_WIDGET_ID env var is set. Centralized Jinja2Templates instance
in web/templates_env.py with jivosite_widget_id as a global.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 14:11:25 +03:00

164 lines
4.9 KiB
Python

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)