-
Notifications
You must be signed in to change notification settings - Fork 28
feat: add vk_id app for VK authentication #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from django.contrib import admin | ||
|
|
||
| # Register your models here. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| from django.apps import AppConfig | ||
|
|
||
|
|
||
| class VkIdConfig(AppConfig): | ||
| default_auto_field = 'django.db.models.BigAutoField' | ||
| name = 'app.services.auth.vk_id' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from django.db import models | ||
|
|
||
| # Create your models here. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from django.test import TestCase | ||
|
|
||
| # Create your tests here. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| from django.urls import path | ||
|
|
||
| from app.services.auth.vk_id import views | ||
|
|
||
| urlpatterns = [ | ||
| path("", views.vk_draft_login, name="vk_login"), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Caution вы вызываете |
||
| path("start/", views.vk_start_auth, name="vk_start"), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Caution вы вызываете |
||
| path("callback/", views.vk_auth_callback, name="vk_callback"), | ||
| ] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Caution отсутствуют |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| import secrets | ||
| import requests | ||
| from urllib.parse import urlencode | ||
| import logging | ||
|
|
||
| from django.conf import settings | ||
| from django.contrib.auth import get_user_model, login | ||
| from django.http import HttpResponse | ||
| from django.shortcuts import redirect | ||
| from inertia import inertia | ||
|
|
||
| User = get_user_model() | ||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class VKOAuthError(Exception): | ||
| """Custom exception for VK OAuth errors""" | ||
| pass | ||
|
|
||
|
|
||
| def validate_state(request): | ||
| state = request.GET.get("state") | ||
| session_state = request.session.get("vk_state") | ||
|
|
||
| if not state or state != session_state: | ||
| logger.warning(f"Invalid state. Expected: {session_state}, Got: {state}") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Warning
|
||
| raise VKOAuthError("Invalid state parameter") | ||
|
|
||
| request.session.pop("vk_state", None) | ||
| logger.debug("State parameter validated") | ||
| return True | ||
|
|
||
|
|
||
| def get_access_token(code: str) -> dict: | ||
| params = { | ||
| "client_id": settings.VK_CLIENT_ID, | ||
| "client_secret": settings.VK_CLIENT_SECRET, | ||
| "redirect_uri": settings.VK_REDIRECT_URI, | ||
| "code": code, | ||
| } | ||
|
|
||
| try: | ||
| res = requests.get("https://oauth.vk.com/access_token", params=params, timeout=10) | ||
| res.raise_for_status() | ||
| data = res.json() | ||
|
|
||
| if "error" in data: | ||
| raise VKOAuthError(f"VK token error: {data['error']}") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tip при |
||
| return data | ||
|
|
||
| except requests.Timeout: | ||
| raise VKOAuthError("Timeout while connecting to VK") | ||
| except requests.RequestException as e: | ||
| logger.error(f"RequestException during token fetch: {e}") | ||
| raise VKOAuthError(f"Failed to get VK access token: {str(e)}") | ||
|
|
||
|
|
||
| def get_vk_user_info(user_id: str, access_token: str) -> dict: | ||
| params = { | ||
| "user_ids": user_id, | ||
| "fields": "first_name,last_name", | ||
| "access_token": access_token, | ||
| "v": "5.131", | ||
| } | ||
|
|
||
| try: | ||
| res = requests.get("https://api.vk.com/method/users.get", params=params, timeout=10) | ||
| res.raise_for_status() | ||
| data = res.json() | ||
|
|
||
| if "error" in data: | ||
| raise VKOAuthError(f"VK API error: {data['error'].get('error_msg', 'Unknown error')}") | ||
| if not data.get("response"): | ||
| raise VKOAuthError("No user data in VK response") | ||
|
|
||
| return data["response"][0] | ||
|
|
||
| except requests.Timeout: | ||
| raise VKOAuthError("Timeout while fetching VK user info") | ||
| except requests.RequestException as e: | ||
| raise VKOAuthError(f"Failed to fetch VK user info: {str(e)}") | ||
|
|
||
|
|
||
| def get_or_create_user(email: str, user_id: str, user_info: dict) -> User: | ||
| if not email: | ||
| email = f"vk_user_{user_id}@fake.vk.com" | ||
| logger.debug(f"Generated fake email: {email}") | ||
|
|
||
| user, created = User.objects.get_or_create( | ||
| email=email, | ||
| defaults={ | ||
| "username": f"vk_{user_id}", | ||
| "first_name": user_info.get("first_name", ""), | ||
| "last_name": user_info.get("last_name", ""), | ||
| }, | ||
| ) | ||
|
|
||
| if created: | ||
| user.set_unusable_password() | ||
| user.save() | ||
| logger.info(f"Created new user: {email}") | ||
| else: | ||
| logger.debug(f"Existing user found: {email}") | ||
|
|
||
| return user | ||
|
|
||
|
|
||
| def vk_auth_callback(request): | ||
| code = request.GET.get("code") | ||
| if not code: | ||
| return HttpResponse("Authorization code not provided", status=400) | ||
|
|
||
| try: | ||
| validate_state(request) | ||
| token_data = get_access_token(code) | ||
| access_token = token_data.get("access_token") | ||
| email = token_data.get("email") | ||
| user_id = token_data.get("user_id") | ||
|
|
||
| if not access_token or not user_id: | ||
| raise VKOAuthError("Missing access_token or user_id from VK") | ||
|
|
||
| user_info = get_vk_user_info(user_id, access_token) | ||
| user = get_or_create_user(email, user_id, user_info) | ||
|
|
||
| login(request, user) | ||
| logger.info(f"User {user.email} logged in via VK") | ||
|
|
||
| redirect_url = getattr(settings, "LOGIN_REDIRECT_URL", "/") | ||
| return inertia.redirect(request, redirect_url) | ||
|
|
||
| except VKOAuthError as e: | ||
| logger.error(f"VK OAuth error: {str(e)}") | ||
| return HttpResponse(str(e), status=400) | ||
| except (requests.RequestException, requests.Timeout) as e: | ||
| logger.error(f"VK API request failed: {str(e)}") | ||
| return HttpResponse(f"VK API error: {str(e)}", status=400) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tip стиль получения env-переменных сейчас смешанный ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Привести к единому стилю и добавить явную проверку/валидацию VK-конфига (или в |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ | |
|
|
||
| INSTALLED_APPS = [ | ||
| 'app.services.auth.users', | ||
| 'app.services.auth.vk_id', | ||
| 'django.contrib.admin', | ||
| 'django.contrib.auth', | ||
| 'django.contrib.contenttypes', | ||
|
|
@@ -168,3 +169,7 @@ | |
| EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "") | ||
| EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "") | ||
| EMAIL_TIMEOUT = int(os.environ.get("EMAIL_TIMEOUT", 10)) | ||
|
|
||
| VK_CLIENT_ID = os.getenv('VK_CLIENT_ID', '') | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Warning пустые дефолты для критичных VK-полей могут скрывать проблему конфигурации до первого запроса. |
||
| VK_CLIENT_SECRET = os.getenv('VK_CLIENT_SECRET', '') | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Warning пустые дефолты для критичных VK-полей могут скрывать проблему конфигурации до первого запроса. |
||
| VK_REDIRECT_URI = os.getenv('VK_REDIRECT_URI', '') | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Warning пустые дефолты для критичных VK-полей могут скрывать проблему конфигурации до первого запроса. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caution
нужно либо добавить недостающие функции в
app/services/auth/vk_id/views.py, либо убрать эти URL’ы.