import hashlib import secrets from dataclasses import dataclass from datetime import UTC, datetime, timedelta from .database import SiteDatabase from .crypto import TokenCipher def hash_token(token: str) -> bytes: return hashlib.sha256(token.encode("utf-8")).digest() @dataclass(frozen=True) class BindingLink: url: str expires_at: datetime class SiteBindingRepository: """Доступ сайта только к функциям выпуска токенов.""" def __init__(self, database: SiteDatabase) -> None: self.database = database def issue( self, member_id: str, domain: str, bitrix_user_id: int, token_hash: bytes, token_expires_at: datetime, access_token: bytes, refresh_token: bytes, oauth_expires_at: datetime, ) -> None: """Сохраняет в БД информацию о токене, выданном пользователю.""" query = """ SELECT * FROM binding.issue_v1(%s, %s, %s, %s, %s, %s, %s, %s) \ """ with self.database.transaction() as connection: connection.execute( query, ( member_id, domain, bitrix_user_id, token_hash, token_expires_at, access_token, refresh_token, oauth_expires_at, ), ).fetchone() class BindingService: def __init__( self, repository: SiteBindingRepository, cipher: TokenCipher, bot_username: str, ttl_seconds: int, ) -> None: self.repository = repository self.cipher = cipher self.bot_username = bot_username self.ttl_seconds = ttl_seconds def issue( self, member_id: str, domain: str, bitrix_user_id: int, access_token: str, refresh_token: str, auth_expires_seconds: int, ) -> BindingLink: """Выдает ссылку для привязки аккаунта.""" token = secrets.token_urlsafe(32) now = datetime.now(UTC) token_expires_at = now + timedelta(seconds=self.ttl_seconds) oauth_expires_at = now + timedelta(seconds=auth_expires_seconds) self.repository.issue( member_id, domain, bitrix_user_id, hash_token(token), token_expires_at, self.cipher.encrypt(access_token), self.cipher.encrypt(refresh_token), oauth_expires_at, ) return BindingLink( url=f"https://t.me/{self.bot_username}?start=bind_{token}", expires_at=token_expires_at, )