diff --git a/web/config.py b/web/config.py index fabba82..c1f30f7 100644 --- a/web/config.py +++ b/web/config.py @@ -18,6 +18,8 @@ class Settings(BaseSettings): VK_DEFAULT_PHOTO_PATH: str = "/app/default_product.png" + VK_CLIENT_ID: str = "" + VK_CLIENT_SECRET: str = "" VK_API_VERSION: str = "5.131" # Docker compose vars (ignored in app, kept for env compatibility) diff --git a/web/routes/vk.py b/web/routes/vk.py index 8927843..2ce93c8 100644 --- a/web/routes/vk.py +++ b/web/routes/vk.py @@ -1,4 +1,5 @@ from datetime import datetime +from urllib.parse import urlencode import httpx @@ -15,6 +16,57 @@ from web.models import User, VkConnection router = APIRouter(prefix="/vk") VK_API_URL = "https://api.vk.com/method" +VK_OAUTH_URL = "https://oauth.vk.com/authorize" + + +async def _fetch_group_info(token: str) -> tuple[str | None, str | None]: + """Returns (group_id, group_name) for the first admin group, or (None, None).""" + try: + async with httpx.AsyncClient() as client: + resp = await client.get( + f"{VK_API_URL}/groups.get", + params={ + "access_token": token, + "v": settings.VK_API_VERSION, + "filter": "admin", + "extended": 1, + "count": 1, + }, + timeout=15, + ) + if resp.status_code == 200: + data = resp.json() + if "error" not in data: + items = data.get("response", {}).get("items", []) + if items: + return str(items[0].get("id", "")), items[0].get("name") + except Exception: + pass + return None, None + + +def _save_connection(db: Session, user_id: int, token: str, + group_id: str | None, group_name: str | None) -> None: + now = datetime.utcnow() + connection = db.query(VkConnection).filter(VkConnection.user_id == user_id).first() + if connection: + connection.access_token = token + connection.vk_user_id = group_id + connection.first_name = group_name + connection.last_name = None + connection.is_online = True + connection.last_checked_at = now + else: + db.add(VkConnection( + user_id=user_id, + access_token=token, + vk_user_id=group_id, + first_name=group_name, + last_name=None, + is_online=True, + last_checked_at=now, + )) + db.commit() @router.get("") @@ -33,6 +85,46 @@ def vk_page( "user": user, "connection": connection, "error": error, + "vk_client_id": settings.VK_CLIENT_ID, + "callback_url": f"{settings.BASE_URL}/vk/callback", + }) + + +@router.get("/connect") +def vk_connect( + request: Request, + user: User | None = Depends(get_current_user), +): + """Redirect to VK OAuth authorization page.""" + if not user: + return RedirectResponse("/login", 303) + + if not settings.VK_CLIENT_ID: + return RedirectResponse("/vk?error=no_client_id", 303) + + params = urlencode({ + "client_id": settings.VK_CLIENT_ID, + "scope": "market,groups", + "redirect_uri": f"{settings.BASE_URL}/vk/callback", + "display": "page", + "response_type": "token", + "v": settings.VK_API_VERSION, + }) + return RedirectResponse(f"{VK_OAUTH_URL}?{params}", 302) + + +@router.get("/callback") +def vk_callback( + request: Request, + user: User | None = Depends(get_current_user), +): + """Landing page after VK OAuth. JS reads the token from the URL fragment and POSTs it.""" + if not user: + return RedirectResponse("/login", 303) + + return templates.TemplateResponse("vk_callback.html", { + "request": request, + "user": user, }) @@ -42,7 +134,7 @@ async def vk_token( db: Session = Depends(get_db), user: User | None = Depends(get_current_user), ): - """Save a manually entered VK community access token.""" + """Save a VK user access token (from manual entry or OAuth callback).""" if not user: return RedirectResponse("/login", 303) @@ -51,52 +143,11 @@ async def vk_token( if not token: return RedirectResponse("/vk?error=empty_token", 303) - # Fetch community info to validate the token and get group name/id - group_id = None - group_name = None - try: - async with httpx.AsyncClient() as client: - resp = await client.get( - f"{VK_API_URL}/groups.getById", - params={"access_token": token, "v": settings.VK_API_VERSION}, - timeout=15, - ) - if resp.status_code == 200: - data = resp.json() - if "error" in data: - return RedirectResponse("/vk?error=invalid_token", 303) - response = data.get("response", []) - groups = response if isinstance(response, list) else response.get("groups", []) - if groups: - group_id = str(groups[0].get("id", "")) - group_name = groups[0].get("name") - elif resp.status_code == 401: - return RedirectResponse("/vk?error=invalid_token", 303) - except Exception: - pass - - connection = db.query(VkConnection).filter(VkConnection.user_id == user.id).first() - now = datetime.utcnow() - if connection: - connection.access_token = token - connection.vk_user_id = group_id - connection.first_name = group_name - connection.last_name = None - connection.is_online = True - connection.last_checked_at = now - else: - connection = VkConnection( - user_id=user.id, - access_token=token, - vk_user_id=group_id, - first_name=group_name, - last_name=None, - is_online=True, - last_checked_at=now, - ) - db.add(connection) - db.commit() + group_id, group_name = await _fetch_group_info(token) + if not group_id: + return RedirectResponse("/vk?error=invalid_token", 303) + _save_connection(db, user.id, token, group_id, group_name) return RedirectResponse("/connections", 303) diff --git a/web/templates/vk.html b/web/templates/vk.html index f48f2ef..621c749 100644 --- a/web/templates/vk.html +++ b/web/templates/vk.html @@ -8,9 +8,11 @@ {% if error %}
{% if error == "invalid_token" %} - Токен недействителен. Убедитесь, что скопировали ключ доступа сообщества правильно. + Токен недействителен или у него нет прав администратора сообщества. {% elif error == "empty_token" %} - Введите ключ доступа. + Введите токен. + {% elif error == "no_client_id" %} + Автоматическое подключение не настроено. Введите токен вручную. {% else %} Произошла ошибка при подключении: {{ error }} {% endif %} @@ -47,10 +49,10 @@