mirror of
https://github.com/openmax-server/server.git
synced 2026-05-26 05:21:43 +03:00
MAX: Разделил процессоры
This commit is contained in:
454
src/oneme/processors/auth.py
Normal file
454
src/oneme/processors/auth.py
Normal file
@@ -0,0 +1,454 @@
|
||||
import json
|
||||
import secrets
|
||||
import hashlib
|
||||
import time
|
||||
import logging
|
||||
import pydantic
|
||||
from classes.baseprocessor import BaseProcessor
|
||||
from oneme.models import (
|
||||
RequestCodePayloadModel,
|
||||
VerifyCodePayloadModel,
|
||||
AuthConfirmRegisterPayloadModel,
|
||||
LoginPayloadModel,
|
||||
)
|
||||
from oneme.config import OnemeConfig
|
||||
from common.sms import send_sms_code
|
||||
|
||||
|
||||
class AuthProcessors(BaseProcessor):
|
||||
def __init__(self, db_pool=None, clients=None, send_event=None, telegram_bot=None, type="socket"):
|
||||
super().__init__(db_pool, clients, send_event, type)
|
||||
self.server_config = OnemeConfig().SERVER_CONFIG
|
||||
self.telegram_bot = telegram_bot
|
||||
|
||||
async def auth_request(self, payload, seq, writer):
|
||||
"""Обработчик запроса кода"""
|
||||
try:
|
||||
RequestCodePayloadModel.model_validate(payload)
|
||||
except pydantic.ValidationError as error:
|
||||
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
|
||||
await self._send_error(seq, self.opcodes.AUTH_REQUEST, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Извлекаем телефон из пакета
|
||||
phone = payload.get("phone").replace("+", "").replace(" ", "").replace("-", "")
|
||||
|
||||
# Генерируем токен
|
||||
token = secrets.token_urlsafe(128)
|
||||
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
# Время истечения токена
|
||||
expires = int(time.time()) + 300
|
||||
|
||||
user_exists = False
|
||||
|
||||
# Ищем пользователя
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,))
|
||||
user = await cursor.fetchone()
|
||||
|
||||
# Получаем код через SMS шлюз или генерируем локально (безопасность прежде всего)
|
||||
if self.config.sms_gateway_url:
|
||||
code = await send_sms_code(self.config.sms_gateway_url, phone)
|
||||
|
||||
if code is None:
|
||||
code = str(secrets.randbelow(900000) + 100000)
|
||||
else:
|
||||
code = str(secrets.randbelow(900000) + 100000)
|
||||
|
||||
# Хешируем
|
||||
code_hash = hashlib.sha256(code.encode()).hexdigest()
|
||||
|
||||
# Сохраняем токен и если нужно отправляем код через тг
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
if user:
|
||||
user_exists = True
|
||||
# Сохраняем токен
|
||||
await cursor.execute(
|
||||
"INSERT INTO auth_tokens (phone, token_hash, code_hash, expires) VALUES (%s, %s, %s, %s)",
|
||||
(phone, token_hash, code_hash, expires,)
|
||||
)
|
||||
|
||||
# Если тг бот включен, и тг привязан к аккаунту - отправляем туда сообщение
|
||||
if not self.config.sms_gateway_url and self.telegram_bot and user.get("telegram_id"):
|
||||
await self.telegram_bot.send_code(chat_id=int(user.get("telegram_id")), phone=phone, code=code)
|
||||
else:
|
||||
# Пользователь не найден - сохраняем токен со state='register'
|
||||
# чтобы после верификации кода направить на экран регистрации
|
||||
await cursor.execute(
|
||||
"INSERT INTO auth_tokens (phone, token_hash, code_hash, expires, state) VALUES (%s, %s, %s, %s, %s)",
|
||||
(phone, token_hash, code_hash, expires, "register",)
|
||||
)
|
||||
|
||||
# Данные пакета
|
||||
payload = {
|
||||
"requestMaxDuration": 60000,
|
||||
"requestCountLeft": 10,
|
||||
"altActionDuration": 60000,
|
||||
"codeLength": 6,
|
||||
"token": token
|
||||
}
|
||||
|
||||
# Собираем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_REQUEST, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
self.logger.debug(f"Код для {phone}: {code} (существующий={user_exists})")
|
||||
|
||||
async def auth(self, payload, seq, writer, deviceType, deviceName):
|
||||
"""Обработчик проверки кода"""
|
||||
try:
|
||||
VerifyCodePayloadModel.model_validate(payload)
|
||||
except pydantic.ValidationError as error:
|
||||
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
|
||||
await self._send_error(seq, self.opcodes.AUTH, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Извлекаем данные из пакета
|
||||
code = payload.get("verifyCode")
|
||||
token = payload.get("token")
|
||||
|
||||
# Хешируем токен с кодом
|
||||
hashed_code = hashlib.sha256(code.encode()).hexdigest()
|
||||
hashed_token = hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
# Генерируем постоянный токен
|
||||
login = secrets.token_urlsafe(128)
|
||||
hashed_login = hashlib.sha256(login.encode()).hexdigest()
|
||||
|
||||
# Ищем токен с кодом
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# Ищем токен
|
||||
await cursor.execute(
|
||||
"SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()",
|
||||
(hashed_token,)
|
||||
)
|
||||
stored_token = await cursor.fetchone()
|
||||
|
||||
# Если токен просрочен, или его нет - отправляем ошибку
|
||||
if stored_token is None:
|
||||
await self._send_error(seq, self.opcodes.AUTH, self.error_types.CODE_EXPIRED, writer)
|
||||
return
|
||||
|
||||
# Проверяем код
|
||||
if stored_token.get("code_hash") != hashed_code:
|
||||
await self._send_error(seq, self.opcodes.AUTH, self.error_types.INVALID_CODE, writer)
|
||||
return
|
||||
|
||||
# Если это новый пользователь - переводим токен в state='verified'
|
||||
# и отдаём клиенту REGISTER токен, чтобы он показал экран ввода имени
|
||||
if stored_token.get("state") == "register":
|
||||
await cursor.execute(
|
||||
"UPDATE auth_tokens SET state = %s WHERE token_hash = %s",
|
||||
("verified", hashed_token,)
|
||||
)
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH,
|
||||
payload={
|
||||
"tokenAttrs": {
|
||||
"REGISTER": {
|
||||
"token": token
|
||||
}
|
||||
},
|
||||
"presetAvatars": []
|
||||
}
|
||||
)
|
||||
await self._send(writer, packet)
|
||||
return
|
||||
|
||||
# Ищем аккаунт
|
||||
await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
|
||||
account = await cursor.fetchone()
|
||||
|
||||
# Удаляем токен
|
||||
await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
|
||||
|
||||
# Создаем сессию
|
||||
await cursor.execute(
|
||||
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
|
||||
(stored_token.get("phone"), hashed_login, deviceType, deviceName, "Little Saint James Island", int(time.time()),) # весь покрытый зеленью, абсолютно весь, остров невезения в океане есть
|
||||
)
|
||||
|
||||
# Генерируем профиль
|
||||
# Аватарка с биографией
|
||||
photoId = None if not account.get("avatar_id") else int(account.get("avatar_id"))
|
||||
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
|
||||
description = None if not account.get("description") else account.get("description")
|
||||
|
||||
# Собираем данные пакета
|
||||
payload = {
|
||||
"tokenAttrs": {
|
||||
"LOGIN": {
|
||||
"token": login
|
||||
}
|
||||
},
|
||||
"profile": self.tools.generate_profile(
|
||||
id=account.get("id"),
|
||||
phone=int(account.get("phone")),
|
||||
avatarUrl=avatar_url,
|
||||
photoId=photoId,
|
||||
updateTime=int(account.get("updatetime")),
|
||||
firstName=account.get("firstname"),
|
||||
lastName=account.get("lastname"),
|
||||
options=json.loads(account.get("options")),
|
||||
description=description,
|
||||
accountStatus=int(account.get("accountstatus")),
|
||||
profileOptions=json.loads(account.get("profileoptions")),
|
||||
includeProfileOptions=True,
|
||||
username=account.get("username")
|
||||
)
|
||||
}
|
||||
|
||||
# Создаем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
|
||||
async def auth_confirm(self, payload, seq, writer, deviceType, deviceName):
|
||||
"""Обработчик подтверждения регистрации нового пользователя"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
AuthConfirmRegisterPayloadModel.model_validate(payload)
|
||||
except pydantic.ValidationError as error:
|
||||
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
|
||||
await self._send_error(seq, self.opcodes.AUTH_CONFIRM, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Извлекаем данные из пакета
|
||||
token = payload.get("token")
|
||||
first_name = payload.get("firstName").strip()
|
||||
last_name = (payload.get("lastName") or "").strip()
|
||||
|
||||
# Хешируем токен
|
||||
hashed_token = hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
# Генерируем постоянный логин-токен
|
||||
login = secrets.token_urlsafe(128)
|
||||
hashed_login = hashlib.sha256(login.encode()).hexdigest()
|
||||
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# Ищем токен - он должен быть в state='verified'
|
||||
await cursor.execute(
|
||||
"SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP() AND state = %s",
|
||||
(hashed_token, "verified",)
|
||||
)
|
||||
stored_token = await cursor.fetchone()
|
||||
|
||||
# Если токен не найден или просрочен - отправляем ошибку
|
||||
if stored_token is None:
|
||||
await self._send_error(seq, self.opcodes.AUTH_CONFIRM, self.error_types.CODE_EXPIRED, writer)
|
||||
return
|
||||
|
||||
phone = stored_token.get("phone")
|
||||
|
||||
# Проверяем что пользователь с таким телефоном ещё не существует
|
||||
await cursor.execute("SELECT id FROM users WHERE phone = %s", (phone,))
|
||||
if await cursor.fetchone():
|
||||
await self._send_error(seq, self.opcodes.AUTH_CONFIRM, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
now_ms = int(time.time() * 1000)
|
||||
now_s = int(time.time())
|
||||
|
||||
# Создаем пользователя
|
||||
await cursor.execute(
|
||||
"""
|
||||
INSERT INTO users
|
||||
(id, phone, telegram_id, firstname, lastname, username,
|
||||
profileoptions, options, accountstatus, updatetime, lastseen)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
self.tools.generate_id(), phone, None, first_name, last_name, None,
|
||||
json.dumps([]), json.dumps(["TT", "ONEME"]),
|
||||
0, str(now_ms), str(now_s),
|
||||
)
|
||||
)
|
||||
|
||||
user_id = cursor.lastrowid
|
||||
|
||||
# Добавляем данные аккаунта
|
||||
await cursor.execute(
|
||||
"""
|
||||
INSERT INTO user_data
|
||||
(phone, contacts, folders, user_config, chat_config)
|
||||
VALUES (%s %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
phone,
|
||||
json.dumps([]),
|
||||
json.dumps(self.static.USER_FOLDERS),
|
||||
json.dumps(self.static.USER_SETTINGS),
|
||||
json.dumps({}),
|
||||
)
|
||||
)
|
||||
|
||||
# Удаляем токен
|
||||
await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
|
||||
|
||||
# Создаем сессию
|
||||
await cursor.execute(
|
||||
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
|
||||
(phone, hashed_login, deviceType or "ANDROID", deviceName or "Unknown", "Little Saint James Island", now_s,)
|
||||
)
|
||||
|
||||
# Генерируем профиль
|
||||
profile = self.tools.generate_profile(
|
||||
id=user_id,
|
||||
phone=int(phone),
|
||||
avatarUrl=None,
|
||||
photoId=None,
|
||||
updateTime=now_ms,
|
||||
firstName=first_name,
|
||||
lastName=last_name,
|
||||
options=["ONEME"],
|
||||
description=None,
|
||||
accountStatus=0,
|
||||
profileOptions=[],
|
||||
includeProfileOptions=True,
|
||||
username=None
|
||||
)
|
||||
|
||||
# Собираем данные пакета
|
||||
payload = {
|
||||
"userToken": "0",
|
||||
"profile": profile,
|
||||
"tokenType": "LOGIN",
|
||||
"token": login
|
||||
}
|
||||
|
||||
# Создаем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
self.logger.info(f"Новый пользователь зарегистрирован: phone={phone} id={user_id} name={first_name} {last_name}")
|
||||
|
||||
async def login(self, payload, seq, writer):
|
||||
"""Обработчик авторизации клиента на сервере"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
LoginPayloadModel.model_validate(payload)
|
||||
except pydantic.ValidationError as error:
|
||||
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
|
||||
await self._send_error(seq, self.opcodes.LOGIN, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Чаты, где состоит пользователь
|
||||
chats = []
|
||||
|
||||
# Получаем данные из пакета
|
||||
token = payload.get("token")
|
||||
|
||||
# Хешируем токен
|
||||
hashed_token = hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
# Ищем токен в бд
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("SELECT * FROM tokens WHERE token_hash = %s", (hashed_token,))
|
||||
token_data = await cursor.fetchone()
|
||||
|
||||
# Если токен не найден, отправляем ошибку
|
||||
if token_data is None:
|
||||
await self._send_error(seq, self.opcodes.LOGIN, self.error_types.INVALID_TOKEN, writer)
|
||||
return
|
||||
|
||||
# Ищем аккаунт пользователя в бд
|
||||
await cursor.execute("SELECT * FROM users WHERE phone = %s", (token_data.get("phone"),))
|
||||
user = await cursor.fetchone()
|
||||
|
||||
# Ищем данные пользователя в бд
|
||||
await cursor.execute("SELECT * FROM user_data WHERE phone = %s", (token_data.get("phone"),))
|
||||
user_data = await cursor.fetchone()
|
||||
|
||||
# Ищем все чаты, где состоит пользователь
|
||||
await cursor.execute(
|
||||
"SELECT * FROM chat_participants WHERE user_id = %s",
|
||||
(user.get('id'))
|
||||
)
|
||||
user_chats = await cursor.fetchall()
|
||||
|
||||
for chat in user_chats:
|
||||
chats.append(
|
||||
chat.get("chat_id")
|
||||
)
|
||||
|
||||
# Аватарка с биографией
|
||||
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
|
||||
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
|
||||
description = None if not user.get("description") else user.get("description")
|
||||
|
||||
# Генерируем профиль
|
||||
profile = self.tools.generate_profile(
|
||||
id=user.get("id"),
|
||||
phone=int(user.get("phone")),
|
||||
avatarUrl=avatar_url,
|
||||
photoId=photoId,
|
||||
updateTime=int(user.get("updatetime")),
|
||||
firstName=user.get("firstname"),
|
||||
lastName=user.get("lastname"),
|
||||
options=json.loads(user.get("options")),
|
||||
description=description,
|
||||
accountStatus=int(user.get("accountstatus")),
|
||||
profileOptions=json.loads(user.get("profileoptions")),
|
||||
includeProfileOptions=True,
|
||||
username=user.get("username")
|
||||
)
|
||||
|
||||
chats = await self.tools.generate_chats(
|
||||
chats, self.db_pool, user.get("id")
|
||||
)
|
||||
|
||||
# Формируем данные пакета
|
||||
payload = {
|
||||
"profile": profile,
|
||||
"chats": chats,
|
||||
"chatMarker": 0,
|
||||
"messages": {},
|
||||
"contacts": [],
|
||||
"presence": {},
|
||||
"config": {
|
||||
"server": self.server_config,
|
||||
"user": json.loads(user_data.get("user_config"))
|
||||
},
|
||||
"token": token,
|
||||
"videoChatHistory": False,
|
||||
"time": int(time.time() * 1000)
|
||||
}
|
||||
|
||||
# Собираем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.LOGIN, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
return int(user.get("phone")), int(user.get("id")), hashed_token
|
||||
|
||||
async def logout(self, seq, writer, hashedToken):
|
||||
"""Обработчик завершения сессии"""
|
||||
# Удаляем токен из бд
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("DELETE FROM tokens WHERE token_hash = %s", (hashedToken,))
|
||||
|
||||
# Создаем пакет
|
||||
response = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.LOGOUT, payload=None
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, response)
|
||||
Reference in New Issue
Block a user