feat
This commit is contained in:
parent
d6512dac5a
commit
c1efb9d36f
@ -8,12 +8,14 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ header.section(user) }}
|
{{ header.section(user) }}
|
||||||
<form on class="c_sbrxk">
|
<form class="c_sbrxk">
|
||||||
|
<img class="login-illustration" src="/static/login-art.png" alt="" aria-hidden="true">
|
||||||
<div class="c_dtjoh">
|
<div class="c_dtjoh">
|
||||||
<div class="c_aihm6 floating-group">
|
<div class="c_aihm6 floating-group">
|
||||||
<input type="email" id="email" name="email" class="c_xhd5k" required>
|
<input type="text" id="email" name="email" class="c_xhd5k" autocomplete="username" required>
|
||||||
<label for="email" class="c_z3ybu floating-label">Email</label>
|
<label for="email" class="c_z3ybu floating-label">Логин</label>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="login-hint">Можно указать телефон или email от аккаунта</span>
|
||||||
<div class="c_aihm6 floating-group file-group poster-group password-group" style="margin-bottom: 0;">
|
<div class="c_aihm6 floating-group file-group poster-group password-group" style="margin-bottom: 0;">
|
||||||
<input type="password" id="password" name="password" class="c_xhd5k" required>
|
<input type="password" id="password" name="password" class="c_xhd5k" required>
|
||||||
<label for="password" class="c_z3ybu floating-label">Пароль</label>
|
<label for="password" class="c_z3ybu floating-label">Пароль</label>
|
||||||
@ -34,7 +36,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<a href="/restore{% if next %}?next={{next}}{% endif %}" onclick="sendToNextPage(event)" style="margin-bottom: 1rem;">Забыли пароль ?</a>
|
<span class="login-hint">Минимум 8 символов</span>
|
||||||
|
<a href="/restore{% if next %}?next={{next}}{% endif %}" onclick="sendToNextPage(event)" class="login-forgot" style="margin-bottom: 1rem;">Забыли пароль ?</a>
|
||||||
|
|
||||||
<button class="c_aq9et" id="createBtn" type="button">
|
<button class="c_aq9et" id="createBtn" type="button">
|
||||||
<span class="c_som9s">Войти</span>
|
<span class="c_som9s">Войти</span>
|
||||||
@ -42,6 +45,18 @@
|
|||||||
<div style="display: flex; gap:15px">
|
<div style="display: flex; gap:15px">
|
||||||
<span>Нет аккаунта?</span><a href="/registration{% if next %}?next={{next}}{% endif %}">Регистрация</a>
|
<span>Нет аккаунта?</span><a href="/registration{% if next %}?next={{next}}{% endif %}">Регистрация</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="login-social">
|
||||||
|
<div class="login-social__or"><span>или войдите через</span></div>
|
||||||
|
<div class="login-social__row">
|
||||||
|
<a class="login-social__btn login-social__btn--vk" href="/api/v1/social/vk/register{% if next %}?next={{next}}{% endif %}">
|
||||||
|
<svg class="login-social__logo" width="22" height="22" viewBox="0 0 24 24" fill="#0077FF" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<path d="M13.162 18.994c-6.13 0-9.625-4.203-9.77-11.196h3.07c.101 5.132 2.366 7.307 4.16 7.755V7.798h2.89v4.427c1.77-.19 3.63-2.208 4.256-4.427h2.89c-.48 2.734-2.498 4.752-3.933 5.581 1.435.673 3.733 2.432 4.61 5.614h-3.182c-.682-2.125-2.384-3.767-4.64-3.99v3.99h-.34z"/>
|
||||||
|
</svg>
|
||||||
|
<span>VK</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -120,13 +135,18 @@ function sendToNextPage(e) {
|
|||||||
window.location.href = next;
|
window.location.href = next;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// non-2xx — mark all input groups as error
|
|
||||||
const allFields = document.querySelectorAll('.floating-group');
|
const allFields = document.querySelectorAll('.floating-group');
|
||||||
allFields.forEach(g => g.classList.add('error'));
|
let msg;
|
||||||
|
if (res.status === 403) {
|
||||||
// Optional: you can show server message if present
|
// Аккаунт заблокирован — поля ввода ни при чём, показываем причину
|
||||||
// const errorText = await res.text();
|
msg = 'Ваш аккаунт заблокирован администратором.';
|
||||||
// console.error('Server error:', errorText);
|
try { const d = await res.json(); if (d && typeof d.detail === 'string') msg = d.detail; } catch (e) {}
|
||||||
|
allFields.forEach(g => g.classList.remove('error'));
|
||||||
|
} else {
|
||||||
|
msg = 'Неверный логин или пароль';
|
||||||
|
allFields.forEach(g => g.classList.add('error'));
|
||||||
|
}
|
||||||
|
if (typeof showToast === 'function') showToast({ message: msg, status: 'error', duration: 9000 });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Network or other unexpected error — mark fields as error
|
// Network or other unexpected error — mark fields as error
|
||||||
@ -179,6 +199,17 @@ function sendToNextPage(e) {
|
|||||||
pwd.focus();
|
pwd.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Вход через Макс — бэкенд ещё не готов: уведомление, без перехода
|
||||||
|
var maxLoginBtn = document.querySelector('.login-social__btn--max');
|
||||||
|
if (maxLoginBtn) {
|
||||||
|
maxLoginBtn.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast({ message: 'Вход через Макс — в процессе реализации', status: 'info', duration: 2800 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{{ footer.section() }}
|
{{ footer.section() }}
|
||||||
<script>
|
<script>
|
||||||
@ -441,6 +472,43 @@ input[type="number"]:focus::-webkit-inner-spin-button {display: none;}
|
|||||||
}
|
}
|
||||||
.c_sbrxk{z-index: 1000;}
|
.c_sbrxk{z-index: 1000;}
|
||||||
#preview_poster{width: 335px;}
|
#preview_poster{width: 335px;}
|
||||||
|
|
||||||
|
/* «Забыли пароль?» — второстепенная ссылка */
|
||||||
|
.login-forgot { font-size: 13px; color: #888; align-self: flex-start; }
|
||||||
|
.login-forgot:hover { color: #5F1798; text-decoration: underline; }
|
||||||
|
|
||||||
|
/* Подписи-подсказки под полями */
|
||||||
|
.login-hint { font-size: 12px; color: #888; align-self: flex-start; line-height: 1.3; margin-top: -6px; }
|
||||||
|
/* убираем большой нижний отступ у группы, за которой идёт подсказка */
|
||||||
|
.floating-group:has(+ .login-hint) { margin-bottom: 0; }
|
||||||
|
|
||||||
|
/* Микроанимация кнопки «Войти» */
|
||||||
|
.c_aq9et { cursor: pointer; box-shadow: 0 4px 14px -4px rgba(95,23,152,.5); transition: transform .18s, box-shadow .18s, background .18s; }
|
||||||
|
.c_aq9et:hover { background: #6d1ba9; transform: translateY(-2px); box-shadow: 0 10px 24px -6px rgba(95,23,152,.6); }
|
||||||
|
.c_aq9et:active { transform: translateY(0); box-shadow: 0 4px 12px -4px rgba(95,23,152,.5); }
|
||||||
|
@media (prefers-reduced-motion: reduce) { .c_aq9et:hover, .c_aq9et:active { transform: none; } }
|
||||||
|
|
||||||
|
/* Картинка слева от формы (только десктоп) */
|
||||||
|
.login-illustration { display: none; }
|
||||||
|
@media (min-width: 901px) {
|
||||||
|
.login-illustration { display: block; width: 340px; max-width: 38vw; height: auto; align-self: center; }
|
||||||
|
.c_sbrxk { gap: 80px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Вход через VK / Макс */
|
||||||
|
.login-social { display: flex; flex-direction: column; gap: 10px; align-self: stretch; margin-top: 4px; }
|
||||||
|
.login-social__or { display: flex; align-items: center; gap: 10px; color: #999; font-size: 13px; }
|
||||||
|
.login-social__or::before, .login-social__or::after { content: ''; flex: 1; height: 1px; background: #e5e5e5; }
|
||||||
|
.login-social__row { display: flex; gap: 10px; }
|
||||||
|
.login-social__btn { flex: 1 1 0; min-width: 0; display: flex; align-items: center; justify-content: center; gap: 8px; min-height: 48px; border-radius: 8px; background: #fff; border: 2px solid #ddd; color: #111; text-decoration: none; font-size: 15px; font-weight: 600; transition: border-color .18s, box-shadow .18s, transform .18s; }
|
||||||
|
.login-social__btn:hover { transform: translateY(-1px); box-shadow: 0 6px 16px -6px rgba(0,0,0,.28); }
|
||||||
|
.login-social__btn:active { transform: translateY(0); }
|
||||||
|
.login-social__btn--vk:hover { border-color: #0077FF; }
|
||||||
|
.login-social__btn--max:hover { border-color: #7a3ff2; }
|
||||||
|
.login-social__logo { flex: 0 0 auto; display: block; width: 22px; height: 22px; }
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.login-social__btn:hover, .login-social__btn:active { transform: none; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{{ footer.style() }}
|
{{ footer.style() }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
110
token_manager.py
110
token_manager.py
@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import re # регистронезависимый поиск email при VK-регистрации
|
||||||
import hashlib
|
import hashlib
|
||||||
import base64
|
import base64
|
||||||
from datetime import datetime, timedelta, UTC
|
from datetime import datetime, timedelta, UTC
|
||||||
@ -9,7 +10,8 @@ from bson import ObjectId
|
|||||||
from config import *
|
from config import *
|
||||||
from db import mongo_client, redis_client, gridfs_map
|
from db import mongo_client, redis_client, gridfs_map
|
||||||
import asyncio
|
import asyncio
|
||||||
from utils import fetch_bytes
|
import secrets
|
||||||
|
from utils import fetch_bytes, get_password_hash
|
||||||
import io
|
import io
|
||||||
import uuid
|
import uuid
|
||||||
from utils import encrypt, decrypt
|
from utils import encrypt, decrypt
|
||||||
@ -17,7 +19,7 @@ from utils import encrypt, decrypt
|
|||||||
# ---------------------- Константы OAuth ----------------------
|
# ---------------------- Константы OAuth ----------------------
|
||||||
OAUTH_URL = 'https://id.vk.com/oauth2/auth'
|
OAUTH_URL = 'https://id.vk.com/oauth2/auth'
|
||||||
AUTHORIZE_URL = 'https://id.vk.ru/authorize'
|
AUTHORIZE_URL = 'https://id.vk.ru/authorize'
|
||||||
SCOPES = 'offline,wall,photos,groups'
|
SCOPES = 'offline,wall,photos,groups,email' # email — для регистрации через VK
|
||||||
REDIRECT_URL = f'{FULL_DOMAIN}/api/v1/social/vk/token'
|
REDIRECT_URL = f'{FULL_DOMAIN}/api/v1/social/vk/token'
|
||||||
|
|
||||||
|
|
||||||
@ -29,14 +31,21 @@ class TokenManager:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.tokens_coll = mongo_client["vk"]["tokens"]
|
self.tokens_coll = mongo_client["vk"]["tokens"]
|
||||||
|
|
||||||
async def get_auth_url(self, user_id: str) -> str:
|
async def get_auth_url(self, user_id: str = None, purpose: str = "link", next: str = None) -> str:
|
||||||
|
"""purpose: "link" (привязка соцсети) или "register" (вход/регистрация через VK);
|
||||||
|
next — куда вернуть пользователя после успешного входа."""
|
||||||
state = os.urandom(32).hex()
|
state = os.urandom(32).hex()
|
||||||
code_verifier = os.urandom(50).hex()
|
code_verifier = os.urandom(50).hex()
|
||||||
code_challenge = base64.urlsafe_b64encode(
|
code_challenge = base64.urlsafe_b64encode(
|
||||||
hashlib.sha256(code_verifier.encode('utf-8')).digest()
|
hashlib.sha256(code_verifier.encode('utf-8')).digest()
|
||||||
).decode('utf-8').rstrip('=')
|
).decode('utf-8').rstrip('=')
|
||||||
|
|
||||||
oauth_data = {"user_id": user_id, "code_verifier": code_verifier}
|
oauth_data = {
|
||||||
|
"user_id": user_id,
|
||||||
|
"code_verifier": code_verifier,
|
||||||
|
"purpose": purpose,
|
||||||
|
"next": next,
|
||||||
|
}
|
||||||
await redis_client.setex(f"oauth_state:{state}", 60 * 15, json.dumps(oauth_data))
|
await redis_client.setex(f"oauth_state:{state}", 60 * 15, json.dumps(oauth_data))
|
||||||
|
|
||||||
url = (
|
url = (
|
||||||
@ -73,7 +82,10 @@ class TokenManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
oauth_data = json.loads(oauth_data_raw)
|
oauth_data = json.loads(oauth_data_raw)
|
||||||
user_id = oauth_data["user_id"]
|
user_id = oauth_data.get("user_id")
|
||||||
|
purpose = oauth_data.get("purpose", "link")
|
||||||
|
next_url = oauth_data.get("next")
|
||||||
|
created = False
|
||||||
code_verifier = oauth_data["code_verifier"]
|
code_verifier = oauth_data["code_verifier"]
|
||||||
|
|
||||||
# Выполняем запрос к VK для обмена кода на токены
|
# Выполняем запрос к VK для обмена кода на токены
|
||||||
@ -103,6 +115,13 @@ class TokenManager:
|
|||||||
})
|
})
|
||||||
vk_profile = reqp.json()['response'][0]
|
vk_profile = reqp.json()['response'][0]
|
||||||
|
|
||||||
|
# Режим регистрации: находим или создаём пользователя ДО сохранения соцсетей,
|
||||||
|
# чтобы дальше socials/tokens сохранились уже с реальным user_id.
|
||||||
|
if purpose == "register" and not user_id:
|
||||||
|
user_id, created = await self._resolve_user_for_register(
|
||||||
|
vk_profile, token_data.get("email")
|
||||||
|
)
|
||||||
|
|
||||||
reqg = await client.post(f"{VK_API_URI}groups.get", data={
|
reqg = await client.post(f"{VK_API_URI}groups.get", data={
|
||||||
'access_token':access_token,
|
'access_token':access_token,
|
||||||
'extended':1,
|
'extended':1,
|
||||||
@ -111,8 +130,11 @@ class TokenManager:
|
|||||||
})
|
})
|
||||||
vk_groups = reqg.json()['response']['items']
|
vk_groups = reqg.json()['response']['items']
|
||||||
urls = [vk_profile['photo_max_orig']] + [i['photo_200'] for i in vk_groups]
|
urls = [vk_profile['photo_max_orig']] + [i['photo_200'] for i in vk_groups]
|
||||||
results = await asyncio.gather(*(fetch_bytes(client, u) for u in urls))
|
image_bytes = await asyncio.gather(*(fetch_bytes(client, u) for u in urls))
|
||||||
results = [await self.save_vk(i) for i in results]
|
# В режиме регистрации ставим аватар VK в профиль, если фото не задано вручную
|
||||||
|
if purpose == "register" and user_id and image_bytes:
|
||||||
|
await self._maybe_set_vk_avatar(user_id, created, image_bytes[0])
|
||||||
|
results = [await self.save_vk(i) for i in image_bytes]
|
||||||
socials = [{
|
socials = [{
|
||||||
'name': f"{vk_profile['last_name']} {vk_profile['first_name']}",
|
'name': f"{vk_profile['last_name']} {vk_profile['first_name']}",
|
||||||
'screen_name': vk_profile['screen_name'],
|
'screen_name': vk_profile['screen_name'],
|
||||||
@ -142,7 +164,12 @@ class TokenManager:
|
|||||||
# Удаляем использованный state, чтобы его нельзя было переиспользовать.
|
# Удаляем использованный state, чтобы его нельзя было переиспользовать.
|
||||||
await redis_client.delete(key)
|
await redis_client.delete(key)
|
||||||
|
|
||||||
return token_data
|
return {
|
||||||
|
"user_id": user_id,
|
||||||
|
"registered": created,
|
||||||
|
"purpose": purpose,
|
||||||
|
"next": next_url,
|
||||||
|
}
|
||||||
|
|
||||||
# Обработка ошибок HTTP (напр., неверный код, истёк скоп)
|
# Обработка ошибок HTTP (напр., неверный код, истёк скоп)
|
||||||
except httpx.HTTPStatusError as e:
|
except httpx.HTTPStatusError as e:
|
||||||
@ -157,6 +184,73 @@ class TokenManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _resolve_user_for_register(self, vk_profile: dict, email: str | None):
|
||||||
|
"""Находит пользователя по vk_id или email, иначе создаёт нового.
|
||||||
|
|
||||||
|
Возвращает кортеж (user_id: str, created: bool).
|
||||||
|
При совпадении по email — привязывает vk_id к найденному аккаунту.
|
||||||
|
"""
|
||||||
|
users = mongo_client["auth"]["users"]
|
||||||
|
vk_id = vk_profile.get("id")
|
||||||
|
|
||||||
|
# 1) Уже привязанный VK-аккаунт
|
||||||
|
existing = await users.find_one({"vk_id": vk_id}, {"_id": 1})
|
||||||
|
if existing:
|
||||||
|
return str(existing["_id"]), False
|
||||||
|
|
||||||
|
# 2) Существующий аккаунт с таким email — логиним и привязываем VK
|
||||||
|
if email:
|
||||||
|
existing = await users.find_one(
|
||||||
|
{"email": {"$regex": f"^{re.escape(email)}$", "$options": "i"}},
|
||||||
|
{"_id": 1},
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
await users.update_one({"_id": existing["_id"]}, {"$set": {"vk_id": vk_id}})
|
||||||
|
return str(existing["_id"]), False
|
||||||
|
|
||||||
|
# 3) Создаём нового пользователя (роль по умолчанию — художник)
|
||||||
|
first = vk_profile.get("first_name", "") or ""
|
||||||
|
last = vk_profile.get("last_name", "") or ""
|
||||||
|
doc = {
|
||||||
|
"username": f"{first} {last}".strip(),
|
||||||
|
"female_name_lastname": f"{last} {first}".strip(),
|
||||||
|
"email": email,
|
||||||
|
"phone": None,
|
||||||
|
"img": "/static/painter.svg",
|
||||||
|
"roles": ["user"],
|
||||||
|
"extra_scopes": [],
|
||||||
|
"vk_id": vk_id,
|
||||||
|
"client_id": "vk",
|
||||||
|
"living_place": None,
|
||||||
|
"offer_accepted": False,
|
||||||
|
# Неиспользуемый случайный пароль: вход по email/паролю невозможен,
|
||||||
|
# но login-эндпоинт не падает на None.startswith(...).
|
||||||
|
"hashed_password": get_password_hash(secrets.token_urlsafe(32)),
|
||||||
|
}
|
||||||
|
res = await users.insert_one(doc)
|
||||||
|
return str(res.inserted_id), True
|
||||||
|
|
||||||
|
async def _maybe_set_vk_avatar(self, user_id: str, created: bool, avatar_bytes: bytes):
|
||||||
|
"""Ставит аватар из VK в профиль, если пользователь не задал своё фото.
|
||||||
|
|
||||||
|
«Не задан вручную» = img пустой или дефолтная заглушка (/static/...).
|
||||||
|
Для только что созданного пользователя ставим всегда.
|
||||||
|
"""
|
||||||
|
if not avatar_bytes:
|
||||||
|
return
|
||||||
|
users = mongo_client["auth"]["users"]
|
||||||
|
if not created:
|
||||||
|
u = await users.find_one({"_id": ObjectId(user_id)}, {"img": 1})
|
||||||
|
img = (u or {}).get("img")
|
||||||
|
if img and not str(img).startswith("/static/"):
|
||||||
|
return # у пользователя уже своё фото — не трогаем
|
||||||
|
try:
|
||||||
|
grid = gridfs_map["users"]
|
||||||
|
file_id = await grid.upload_from_stream(f"vk_{user_id}.jpg", io.BytesIO(avatar_bytes))
|
||||||
|
await users.update_one({"_id": ObjectId(user_id)}, {"$set": {"img": str(file_id)}})
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"[TokenManager] VK avatar save failed: {e}")
|
||||||
|
|
||||||
# ====================== ШИФРОВАНИЕ ======================
|
# ====================== ШИФРОВАНИЕ ======================
|
||||||
async def _encrypt_token_fields(self, token_data: dict) -> dict:
|
async def _encrypt_token_fields(self, token_data: dict) -> dict:
|
||||||
"""Шифрует access_token и refresh_token."""
|
"""Шифрует access_token и refresh_token."""
|
||||||
|
|||||||
42
vk.ipynb
42
vk.ipynb
@ -46,8 +46,13 @@
|
|||||||
"\n",
|
"\n",
|
||||||
"try:\n",
|
"try:\n",
|
||||||
" load_env('env')\n",
|
" load_env('env')\n",
|
||||||
"except:\n",
|
"except Exception:\n",
|
||||||
" load_env('env_example')"
|
" # env_example тоже может отсутствовать (в образ эти файлы не копируются) —\n",
|
||||||
|
" # тогда переменные берутся из окружения контейнера/k8s-секрета\n",
|
||||||
|
" try:\n",
|
||||||
|
" load_env('env_example')\n",
|
||||||
|
" except Exception:\n",
|
||||||
|
" pass"
|
||||||
],
|
],
|
||||||
"display_name": ".venv"
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
@ -78,7 +83,7 @@
|
|||||||
"from celery import Celery\n",
|
"from celery import Celery\n",
|
||||||
"from bson import ObjectId\n",
|
"from bson import ObjectId\n",
|
||||||
"from fastapi import status\n",
|
"from fastapi import status\n",
|
||||||
"from fastapi import APIRouter,Depends\n",
|
"from fastapi import APIRouter, Depends, Request, Response\n",
|
||||||
"from fastapi.responses import RedirectResponse\n",
|
"from fastapi.responses import RedirectResponse\n",
|
||||||
"from images import insert_logo\n"
|
"from images import insert_logo\n"
|
||||||
],
|
],
|
||||||
@ -130,10 +135,29 @@
|
|||||||
" return RedirectResponse(url=url, status_code=status.HTTP_303_SEE_OTHER)\n",
|
" return RedirectResponse(url=url, status_code=status.HTTP_303_SEE_OTHER)\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
"@router.get(\"/register\")\n",
|
||||||
|
"async def vk_register_start(next: str = '/profile'):\n",
|
||||||
|
" \"\"\"Старт регистрации/входа через VK (без предварительной авторизации).\"\"\"\n",
|
||||||
|
" url = await token_manager.get_auth_url(purpose='register', next=next)\n",
|
||||||
|
" return RedirectResponse(url=url, status_code=status.HTTP_303_SEE_OTHER)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
"@router.get(\"/token\")\n",
|
"@router.get(\"/token\")\n",
|
||||||
"async def oauth_callback(code: str, state: str, device_id: str = None):\n",
|
"async def oauth_callback(request: Request, response: Response, code: str, state: str, device_id: str = None):\n",
|
||||||
" token_data = await token_manager.get_token(code=code, state=state, device_id=device_id)\n",
|
" result = await token_manager.get_token(code=code, state=state, device_id=device_id)\n",
|
||||||
" return RedirectResponse(url='/profile/social', status_code=status.HTTP_303_SEE_OTHER)\n"
|
" if not result or not result.get('user_id'):\n",
|
||||||
|
" return RedirectResponse(url='/login', status_code=status.HTTP_303_SEE_OTHER)\n",
|
||||||
|
"\n",
|
||||||
|
" # Регистрация/вход через VK — выдаём нашу сессию и редиректим на next\n",
|
||||||
|
" if result.get('purpose') == 'register':\n",
|
||||||
|
" await issue_session(result['user_id'], request, response, client_id='vk')\n",
|
||||||
|
" next_url = result.get('next') or '/profile'\n",
|
||||||
|
" if not is_safe_redirect(next_url):\n",
|
||||||
|
" next_url = '/profile'\n",
|
||||||
|
" return RedirectResponse(url=next_url, status_code=status.HTTP_303_SEE_OTHER, headers=response.headers)\n",
|
||||||
|
"\n",
|
||||||
|
" # Привязка соцсети к уже залогиненному пользователю\n",
|
||||||
|
" return RedirectResponse(url='/profile/social', status_code=status.HTTP_303_SEE_OTHER)"
|
||||||
],
|
],
|
||||||
"display_name": ".venv"
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
@ -175,7 +199,6 @@
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"# async def send_vk(item: dict, text: str, file_ids: list, doc: dict, sign_avatar: bool = True): TODO\n",
|
|
||||||
"@router.post(\"/send\")\n",
|
"@router.post(\"/send\")\n",
|
||||||
"async def send_vk(data:dict, request: Request):\n",
|
"async def send_vk(data:dict, request: Request):\n",
|
||||||
" auth = request.headers.get(\"Authorization\")\n",
|
" auth = request.headers.get(\"Authorization\")\n",
|
||||||
@ -250,7 +273,6 @@
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"# async def edit_vk(item: dict, text: str, file_ids: list, doc: dict, sign_avatar: bool = True): TODO\n",
|
|
||||||
"@router.post(\"/edit\")\n",
|
"@router.post(\"/edit\")\n",
|
||||||
"async def edit_vk(data:dict, request: Request):\n",
|
"async def edit_vk(data:dict, request: Request):\n",
|
||||||
" auth = request.headers.get(\"Authorization\")\n",
|
" auth = request.headers.get(\"Authorization\")\n",
|
||||||
@ -265,6 +287,7 @@
|
|||||||
" doc = data['doc']\n",
|
" doc = data['doc']\n",
|
||||||
" post_id = data['post_id']\n",
|
" post_id = data['post_id']\n",
|
||||||
" sign_avatar = data['sign_avatar']\n",
|
" sign_avatar = data['sign_avatar']\n",
|
||||||
|
"# async def edit_vk(item: dict, text: str, file_ids: list, doc: dict, sign_avatar: bool = True):\n",
|
||||||
" db_name = item.get('db')\n",
|
" db_name = item.get('db')\n",
|
||||||
" if db_name == 'competitions':\n",
|
" if db_name == 'competitions':\n",
|
||||||
" social = await social_collection.find_one({'service': True, 'type':'vk'})\n",
|
" social = await social_collection.find_one({'service': True, 'type':'vk'})\n",
|
||||||
@ -327,7 +350,6 @@
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"# TODO сделать вызов функции из @celery_app_vk.task( а не по url в router\n",
|
|
||||||
"@router.post(\"/delete\")\n",
|
"@router.post(\"/delete\")\n",
|
||||||
"async def wall_delete(data: dict, request: Request):\n",
|
"async def wall_delete(data: dict, request: Request):\n",
|
||||||
" # ---- auth ----\n",
|
" # ---- auth ----\n",
|
||||||
@ -1614,7 +1636,7 @@
|
|||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"kernelspec": {
|
"kernelspec": {
|
||||||
"display_name": ".venv (3.14.6.final.0)",
|
"display_name": ".venv",
|
||||||
"language": "python",
|
"language": "python",
|
||||||
"name": "python3"
|
"name": "python3"
|
||||||
},
|
},
|
||||||
|
|||||||
41
vk.py
41
vk.py
@ -31,8 +31,13 @@ def load_env(file_path):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
load_env('env')
|
load_env('env')
|
||||||
except:
|
except Exception:
|
||||||
load_env('env_example')
|
# env_example тоже может отсутствовать (в образ эти файлы не копируются) —
|
||||||
|
# тогда переменные берутся из окружения контейнера/k8s-секрета
|
||||||
|
try:
|
||||||
|
load_env('env_example')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
## Class
|
## Class
|
||||||
|
|
||||||
@ -48,7 +53,7 @@ from io import BytesIO
|
|||||||
from celery import Celery
|
from celery import Celery
|
||||||
from bson import ObjectId
|
from bson import ObjectId
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
from fastapi import APIRouter,Depends
|
from fastapi import APIRouter, Depends, Request, Response
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
from images import insert_logo
|
from images import insert_logo
|
||||||
|
|
||||||
@ -70,12 +75,30 @@ async def auth_redirect(
|
|||||||
return RedirectResponse(url=url, status_code=status.HTTP_303_SEE_OTHER)
|
return RedirectResponse(url=url, status_code=status.HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/token")
|
@router.get("/register")
|
||||||
async def oauth_callback(code: str, state: str, device_id: str = None):
|
async def vk_register_start(next: str = '/profile'):
|
||||||
token_data = await token_manager.get_token(code=code, state=state, device_id=device_id)
|
"""Старт регистрации/входа через VK (без предварительной авторизации)."""
|
||||||
return RedirectResponse(url='/profile/social', status_code=status.HTTP_303_SEE_OTHER)
|
url = await token_manager.get_auth_url(purpose='register', next=next)
|
||||||
|
return RedirectResponse(url=url, status_code=status.HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/token")
|
||||||
|
async def oauth_callback(request: Request, response: Response, code: str, state: str, device_id: str = None):
|
||||||
|
result = await token_manager.get_token(code=code, state=state, device_id=device_id)
|
||||||
|
if not result or not result.get('user_id'):
|
||||||
|
return RedirectResponse(url='/login', status_code=status.HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
|
# Регистрация/вход через VK — выдаём нашу сессию и редиректим на next
|
||||||
|
# if result.get('purpose') == 'register':
|
||||||
|
# await issue_session(result['user_id'], request, response, client_id='vk')
|
||||||
|
# next_url = result.get('next') or '/profile'
|
||||||
|
# if not is_safe_redirect(next_url):
|
||||||
|
# next_url = '/profile'
|
||||||
|
# return RedirectResponse(url=next_url, status_code=status.HTTP_303_SEE_OTHER, headers=response.headers)
|
||||||
|
|
||||||
|
# Привязка соцсети к уже залогиненному пользователю
|
||||||
|
return RedirectResponse(url='/profile/social', status_code=status.HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
async def get_images(db_name: str, file_ids: list, sign_avatar: bool = True):
|
async def get_images(db_name: str, file_ids: list, sign_avatar: bool = True):
|
||||||
images = {}
|
images = {}
|
||||||
photo_bytes_list= []
|
photo_bytes_list= []
|
||||||
@ -99,7 +122,6 @@ async def get_images(db_name: str, file_ids: list, sign_avatar: bool = True):
|
|||||||
return images
|
return images
|
||||||
|
|
||||||
|
|
||||||
# async def send_vk(item: dict, text: str, file_ids: list, doc: dict, sign_avatar: bool = True): TODO
|
|
||||||
@router.post("/send")
|
@router.post("/send")
|
||||||
async def send_vk(data:dict, request: Request):
|
async def send_vk(data:dict, request: Request):
|
||||||
auth = request.headers.get("Authorization")
|
auth = request.headers.get("Authorization")
|
||||||
@ -166,7 +188,6 @@ async def send_vk(data:dict, request: Request):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
# async def edit_vk(item: dict, text: str, file_ids: list, doc: dict, sign_avatar: bool = True): TODO
|
|
||||||
@router.post("/edit")
|
@router.post("/edit")
|
||||||
async def edit_vk(data:dict, request: Request):
|
async def edit_vk(data:dict, request: Request):
|
||||||
auth = request.headers.get("Authorization")
|
auth = request.headers.get("Authorization")
|
||||||
@ -181,6 +202,7 @@ async def edit_vk(data:dict, request: Request):
|
|||||||
doc = data['doc']
|
doc = data['doc']
|
||||||
post_id = data['post_id']
|
post_id = data['post_id']
|
||||||
sign_avatar = data['sign_avatar']
|
sign_avatar = data['sign_avatar']
|
||||||
|
# async def edit_vk(item: dict, text: str, file_ids: list, doc: dict, sign_avatar: bool = True):
|
||||||
db_name = item.get('db')
|
db_name = item.get('db')
|
||||||
if db_name == 'competitions':
|
if db_name == 'competitions':
|
||||||
social = await social_collection.find_one({'service': True, 'type':'vk'})
|
social = await social_collection.find_one({'service': True, 'type':'vk'})
|
||||||
@ -235,7 +257,6 @@ async def edit_vk(data:dict, request: Request):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
# TODO сделать вызов функции из @celery_app_vk.task( а не по url в router
|
|
||||||
@router.post("/delete")
|
@router.post("/delete")
|
||||||
async def wall_delete(data: dict, request: Request):
|
async def wall_delete(data: dict, request: Request):
|
||||||
# ---- auth ----
|
# ---- auth ----
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user