diff --git a/app/database/crud/web_api_token.py b/app/database/crud/web_api_token.py index e8c3c71f..c84b9426 100644 --- a/app/database/crud/web_api_token.py +++ b/app/database/crud/web_api_token.py @@ -6,7 +6,6 @@ from typing import Iterable, List, Optional from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import selectinload from app.database.models import WebApiToken @@ -15,12 +14,8 @@ async def list_tokens( db: AsyncSession, *, include_inactive: bool = False, - user_id: Optional[int] = None, ) -> List[WebApiToken]: - query = select(WebApiToken).options(selectinload(WebApiToken.user)) - - if user_id is not None: - query = query.where(WebApiToken.user_id == user_id) + query = select(WebApiToken) if not include_inactive: query = query.where(WebApiToken.is_active.is_(True)) @@ -32,20 +27,12 @@ async def list_tokens( async def get_token_by_id(db: AsyncSession, token_id: int) -> Optional[WebApiToken]: - query = ( - select(WebApiToken) - .options(selectinload(WebApiToken.user)) - .where(WebApiToken.id == token_id) - ) - result = await db.execute(query) - return result.scalar_one_or_none() + return await db.get(WebApiToken, token_id) async def get_token_by_hash(db: AsyncSession, token_hash: str) -> Optional[WebApiToken]: - query = ( - select(WebApiToken) - .options(selectinload(WebApiToken.user)) - .where(WebApiToken.token_hash == token_hash) + query = select(WebApiToken).where( + WebApiToken.token_hash == token_hash ) result = await db.execute(query) return result.scalar_one_or_none() @@ -60,10 +47,8 @@ async def create_token( description: Optional[str] = None, expires_at: Optional[datetime] = None, created_by: Optional[str] = None, - user_id: Optional[int] = None, ) -> WebApiToken: token = WebApiToken( - user_id=user_id, name=name, token_hash=token_hash, token_prefix=token_prefix, diff --git a/app/database/models.py b/app/database/models.py index 8ac07338..1780d75a 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -387,7 +387,6 @@ class User(Base): referral_earnings = relationship("ReferralEarning", foreign_keys="ReferralEarning.user_id", back_populates="user") discount_offers = relationship("DiscountOffer", back_populates="user") promo_offer_logs = relationship("PromoOfferLog", back_populates="user") - web_api_tokens = relationship("WebApiToken", back_populates="user") lifetime_used_traffic_bytes = Column(BigInteger, default=0) auto_promo_group_assigned = Column(Boolean, nullable=False, default=False) auto_promo_group_threshold_kopeks = Column(BigInteger, nullable=False, default=0) @@ -1239,7 +1238,6 @@ class WebApiToken(Base): __tablename__ = "web_api_tokens" id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True) name = Column(String(255), nullable=False) token_hash = Column(String(128), nullable=False, unique=True, index=True) token_prefix = Column(String(32), nullable=False, index=True) @@ -1251,7 +1249,6 @@ class WebApiToken(Base): last_used_ip = Column(String(64), nullable=True) is_active = Column(Boolean, default=True, nullable=False) created_by = Column(String(255), nullable=True) - user = relationship("User", back_populates="web_api_tokens") def __repr__(self) -> str: status = "active" if self.is_active else "revoked" diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py index f6e112d0..90d637ad 100644 --- a/app/database/universal_migration.py +++ b/app/database/universal_migration.py @@ -2434,7 +2434,6 @@ async def create_web_api_tokens_table() -> bool: create_sql = """ CREATE TABLE web_api_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NULL, name VARCHAR(255) NOT NULL, token_hash VARCHAR(128) NOT NULL UNIQUE, token_prefix VARCHAR(32) NOT NULL, @@ -2445,19 +2444,16 @@ async def create_web_api_tokens_table() -> bool: last_used_at DATETIME NULL, last_used_ip VARCHAR(64) NULL, is_active BOOLEAN NOT NULL DEFAULT 1, - created_by VARCHAR(255) NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL + created_by VARCHAR(255) NULL ); CREATE INDEX idx_web_api_tokens_active ON web_api_tokens(is_active); CREATE INDEX idx_web_api_tokens_prefix ON web_api_tokens(token_prefix); CREATE INDEX idx_web_api_tokens_last_used ON web_api_tokens(last_used_at); - CREATE INDEX ix_web_api_tokens_user_id ON web_api_tokens(user_id); """ elif db_type == "postgresql": create_sql = """ CREATE TABLE web_api_tokens ( id SERIAL PRIMARY KEY, - user_id INTEGER NULL, name VARCHAR(255) NOT NULL, token_hash VARCHAR(128) NOT NULL UNIQUE, token_prefix VARCHAR(32) NOT NULL, @@ -2468,19 +2464,16 @@ async def create_web_api_tokens_table() -> bool: last_used_at TIMESTAMP NULL, last_used_ip VARCHAR(64) NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_by VARCHAR(255) NULL, - CONSTRAINT fk_web_api_tokens_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL + created_by VARCHAR(255) NULL ); CREATE INDEX idx_web_api_tokens_active ON web_api_tokens(is_active); CREATE INDEX idx_web_api_tokens_prefix ON web_api_tokens(token_prefix); CREATE INDEX idx_web_api_tokens_last_used ON web_api_tokens(last_used_at); - CREATE INDEX ix_web_api_tokens_user_id ON web_api_tokens(user_id); """ else: create_sql = """ CREATE TABLE web_api_tokens ( id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NULL, name VARCHAR(255) NOT NULL, token_hash VARCHAR(128) NOT NULL UNIQUE, token_prefix VARCHAR(32) NOT NULL, @@ -2491,8 +2484,7 @@ async def create_web_api_tokens_table() -> bool: last_used_at TIMESTAMP NULL, last_used_ip VARCHAR(64) NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_by VARCHAR(255) NULL, - CONSTRAINT fk_web_api_tokens_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL + created_by VARCHAR(255) NULL ) ENGINE=InnoDB; CREATE INDEX idx_web_api_tokens_active ON web_api_tokens(is_active); CREATE INDEX idx_web_api_tokens_prefix ON web_api_tokens(token_prefix); @@ -2508,71 +2500,6 @@ async def create_web_api_tokens_table() -> bool: return False -async def ensure_web_api_tokens_user_column() -> bool: - column_exists = await check_column_exists("web_api_tokens", "user_id") - index_name = "ix_web_api_tokens_user_id" - constraint_name = "fk_web_api_tokens_user_id" - - try: - async with engine.begin() as conn: - db_type = await get_database_type() - - if not column_exists: - if db_type == "sqlite": - await conn.execute(text("ALTER TABLE web_api_tokens ADD COLUMN user_id INTEGER")) - elif db_type == "postgresql": - await conn.execute(text("ALTER TABLE web_api_tokens ADD COLUMN user_id INTEGER")) - elif db_type == "mysql": - await conn.execute(text("ALTER TABLE web_api_tokens ADD COLUMN user_id INT NULL")) - else: - logger.error(f"Неподдерживаемый тип БД для web_api_tokens.user_id: {db_type}") - return False - logger.info("Добавлена колонка web_api_tokens.user_id") - - if db_type in {"postgresql", "mysql"}: - constraint_exists = await check_constraint_exists("web_api_tokens", constraint_name) - if not constraint_exists: - try: - await conn.execute( - text( - "ALTER TABLE web_api_tokens " - "ADD CONSTRAINT fk_web_api_tokens_user_id " - "FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL" - ) - ) - logger.info("Добавлено ограничение fk_web_api_tokens_user_id") - except Exception as constraint_error: - logger.warning( - "Не удалось создать ограничение fk_web_api_tokens_user_id: %s", - constraint_error, - ) - - index_exists = await check_index_exists("web_api_tokens", index_name) - if not index_exists: - try: - async with engine.begin() as conn: - db_type = await get_database_type() - if db_type == "sqlite": - await conn.execute( - text("CREATE INDEX IF NOT EXISTS ix_web_api_tokens_user_id ON web_api_tokens(user_id)") - ) - elif db_type == "postgresql": - await conn.execute( - text("CREATE INDEX IF NOT EXISTS ix_web_api_tokens_user_id ON web_api_tokens(user_id)") - ) - elif db_type == "mysql": - await conn.execute(text("CREATE INDEX ix_web_api_tokens_user_id ON web_api_tokens(user_id)")) - logger.info("Создан индекс ix_web_api_tokens_user_id") - except Exception as index_error: - logger.warning(f"Не удалось создать индекс ix_web_api_tokens_user_id: {index_error}") - - return True - - except Exception as error: - logger.error(f"Ошибка обновления web_api_tokens.user_id: {error}") - return False - - async def create_privacy_policies_table() -> bool: table_exists = await check_table_exists("privacy_policies") if table_exists: @@ -2872,7 +2799,6 @@ async def run_universal_migration(): web_api_tokens_ready = await create_web_api_tokens_table() if web_api_tokens_ready: logger.info("✅ Таблица web_api_tokens готова") - await ensure_web_api_tokens_user_column() else: logger.warning("⚠️ Проблемы с таблицей web_api_tokens") diff --git a/app/handlers/menu.py b/app/handlers/menu.py index 10a2e6b1..479d5dfb 100644 --- a/app/handlers/menu.py +++ b/app/handlers/menu.py @@ -12,12 +12,10 @@ from app.keyboards.inline import ( get_main_menu_keyboard, get_language_selection_keyboard, get_info_menu_keyboard, - get_api_access_keyboard, ) from app.localization.texts import get_texts, get_rules -from app.database.models import User, WebApiToken +from app.database.models import User from app.database.crud.user_message import get_random_active_message -from app.database.crud import web_api_token as web_api_token_crud from app.services.subscription_checkout_service import ( has_subscription_checkout_draft, should_offer_checkout_resume, @@ -31,8 +29,6 @@ from app.utils.promo_offer import ( from app.services.privacy_policy_service import PrivacyPolicyService from app.services.public_offer_service import PublicOfferService from app.services.faq_service import FaqService -from app.services.web_api_token_service import web_api_token_service -from app.utils.formatters import format_datetime logger = logging.getLogger(__name__) @@ -136,150 +132,6 @@ async def show_info_menu( await callback.answer() -def _build_api_access_caption( - texts, - token: WebApiToken | None, - *, - has_active_token: bool, -) -> str: - header = texts.t("API_ACCESS_HEADER", "🔑 API доступ") - - parts: list[str] = [header] - - if not token: - parts.append( - texts.t( - "API_ACCESS_NO_TOKEN", - ( - "У вас еще нет активного API ключа.\n" - "Нажмите кнопку ниже, чтобы выпустить первый ключ." - ), - ) - ) - else: - created_at = format_datetime(token.created_at) if token.created_at else "—" - last_used = ( - format_datetime(token.last_used_at) - if token.last_used_at - else texts.t("API_ACCESS_LAST_USED_NEVER", "еще не использовался") - ) - last_ip_line = "" - if token.last_used_ip: - last_ip_line = "\n" + texts.t( - "API_ACCESS_LAST_IP_LINE", - "Последний IP: {ip}", - ).format(ip=token.last_used_ip) - - template_key = "API_ACCESS_ACTIVE_TOKEN" if has_active_token else "API_ACCESS_INACTIVE_TOKEN" - parts.append( - texts.t( - template_key, - ( - "Ваш текущий API ключ выпущен {created_at}.\n" - "Первые символы: {prefix}.\n" - "Последнее использование: {last_used}.{last_ip_line}\n" - "Чтобы выпустить новый ключ, нажмите кнопку ниже — предыдущий будет отключен." - ) - if has_active_token - else ( - "Последний ключ выпущен {created_at}.\n" - "Первые символы: {prefix}.\n" - "Последнее использование: {last_used}.{last_ip_line}\n" - "Чтобы получить доступ, создайте новый ключ." - ), - ).format( - created_at=created_at, - prefix=token.token_prefix, - last_used=last_used, - last_ip_line=last_ip_line, - ) - ) - - parts.append( - texts.t( - "API_ACCESS_SECURITY_HINT", - "Берегите ключ и не передавайте его другим людям.", - ) - ) - - return "\n\n".join(part for part in parts if part) - - -async def show_api_access( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - *, - skip_callback_answer: bool = False, -) -> None: - texts = get_texts(db_user.language) - tokens = await web_api_token_crud.list_tokens( - db, - include_inactive=True, - user_id=db_user.id, - ) - - active_token = next((token for token in tokens if token.is_active), None) - latest_token = tokens[0] if tokens else None - token_to_show = active_token or latest_token - - caption = _build_api_access_caption( - texts, - token_to_show, - has_active_token=bool(active_token), - ) - - await edit_or_answer_photo( - callback=callback, - caption=caption, - keyboard=get_api_access_keyboard(db_user.language), - parse_mode="HTML", - ) - - if not skip_callback_answer: - await callback.answer() - - -async def generate_api_access_token( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, -) -> None: - texts = get_texts(db_user.language) - - token_value, token = await web_api_token_service.issue_user_token(db, db_user) - await db.commit() - - logger.info( - "🔑 Пользователь %s выпустил новый API ключ (token_id=%s)", - db_user.telegram_id, - token.id, - ) - - message_text = texts.t( - "API_ACCESS_NEW_TOKEN", - ( - "Ваш новый API ключ:\n" - "{token}\n\n" - "Сохраните его — повторно показать не получится." - ), - ).format(token=token_value) - - await callback.message.answer(message_text, parse_mode="HTML") - - await show_api_access( - callback, - db_user, - db, - skip_callback_answer=True, - ) - - await callback.answer( - texts.t("API_ACCESS_GENERATED_ALERT", "Новый API ключ создан"), - show_alert=True, - ) - - async def show_faq_pages( callback: types.CallbackQuery, db_user: User, @@ -930,16 +782,6 @@ def register_handlers(dp: Dispatcher): F.data == "menu_info", ) - dp.callback_query.register( - show_api_access, - F.data == "menu_api_access", - ) - - dp.callback_query.register( - generate_api_access_token, - F.data == "api_access_generate", - ) - dp.callback_query.register( show_faq_pages, F.data == "menu_faq", diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 847c2ff5..0d86e119 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -314,13 +314,6 @@ def get_info_menu_keyboard( ) ]) - buttons.append([ - InlineKeyboardButton( - text=texts.t("MENU_API_ACCESS", "🔑 API доступ"), - callback_data="menu_api_access", - ) - ]) - buttons.append([ InlineKeyboardButton(text=texts.MENU_RULES, callback_data="menu_rules") ]) @@ -354,22 +347,6 @@ def get_info_menu_keyboard( return InlineKeyboardMarkup(inline_keyboard=buttons) -def get_api_access_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup: - texts = get_texts(language) - - buttons = [ - [ - InlineKeyboardButton( - text=texts.t("API_ACCESS_GENERATE_BUTTON", "🔄 Выпустить ключ"), - callback_data="api_access_generate", - ) - ], - [InlineKeyboardButton(text=texts.BACK, callback_data="menu_info")], - ] - - return InlineKeyboardMarkup(inline_keyboard=buttons) - - def get_happ_download_button_row(texts) -> Optional[List[InlineKeyboardButton]]: if not settings.is_happ_download_button_enabled(): return None diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json index 767340fb..ffa15770 100644 --- a/app/localization/locales/en.json +++ b/app/localization/locales/en.json @@ -305,7 +305,6 @@ "MENU_INFO": "ℹ️ Info", "MENU_INFO_HEADER": "ℹ️ Info", "MENU_INFO_PROMPT": "Choose a section:", - "MENU_API_ACCESS": "🔑 API key", "MENU_PROMOCODE": "🎫 Promo code", "MENU_REFERRALS": "🤝 Referral program", "MENU_RULES": "📋 Service rules", @@ -760,13 +759,3 @@ "ADMIN_PUBLIC_OFFER_RETURN_TO_EDIT": "⬅️ Back to editing", "ADMIN_PUBLIC_OFFER_EDIT_CANCELLED": "Offer editing cancelled." } - "API_ACCESS_HEADER": "🔑 API access", - "API_ACCESS_NO_TOKEN": "You don't have an active API key yet.\nTap the button below to issue your first key.\n\nEach new key disables the previous one.", - "API_ACCESS_LAST_USED_NEVER": "not used yet", - "API_ACCESS_LAST_IP_LINE": "Last IP: {ip}", - "API_ACCESS_ACTIVE_TOKEN": "Your current API key was issued on {created_at}.\nFirst characters: {prefix}.\nLast used: {last_used}.{last_ip_line}\nTap the button below to issue a new key — the previous one will be revoked.", - "API_ACCESS_INACTIVE_TOKEN": "The last key was issued on {created_at}.\nFirst characters: {prefix}.\nLast used: {last_used}.{last_ip_line}\nIssue a new key to regain access — old keys no longer work.", - "API_ACCESS_SECURITY_HINT": "Store the key securely and never share it with anyone.", - "API_ACCESS_GENERATE_BUTTON": "🔄 Issue key", - "API_ACCESS_NEW_TOKEN": "Here is your new API key:\n{token}\n\nSave it now — it cannot be shown again. Previous keys have been revoked.", - "API_ACCESS_GENERATED_ALERT": "New API key issued", diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json index 9de7e32e..06d7a051 100644 --- a/app/localization/locales/ru.json +++ b/app/localization/locales/ru.json @@ -236,17 +236,6 @@ "MENU_INFO_PROMPT": "Выберите раздел:", "MENU_PRIVACY_POLICY": "🛡️ Политика конф.", "MENU_PUBLIC_OFFER": "📄 Оферта", - "MENU_API_ACCESS": "🔑 API доступ", - "API_ACCESS_HEADER": "🔑 API доступ", - "API_ACCESS_NO_TOKEN": "У вас ещё нет активного API ключа.\nНажмите кнопку ниже, чтобы выпустить первый ключ.\n\nКаждый новый ключ отключает предыдущие.", - "API_ACCESS_LAST_USED_NEVER": "ещё не использовался", - "API_ACCESS_LAST_IP_LINE": "Последний IP: {ip}", - "API_ACCESS_ACTIVE_TOKEN": "Ваш текущий API ключ выпущен {created_at}.\nПервые символы: {prefix}.\nПоследнее использование: {last_used}.{last_ip_line}\nЧтобы выпустить новый ключ, нажмите кнопку ниже — предыдущий будет отключен.", - "API_ACCESS_INACTIVE_TOKEN": "Последний ключ выпущен {created_at}.\nПервые символы: {prefix}.\nПоследнее использование: {last_used}.{last_ip_line}\nЧтобы получить доступ, создайте новый ключ — старые больше не работают.", - "API_ACCESS_SECURITY_HINT": "Храните ключ в безопасном месте и не передавайте его другим людям.", - "API_ACCESS_GENERATE_BUTTON": "🔄 Выпустить ключ", - "API_ACCESS_NEW_TOKEN": "Вот ваш новый API ключ:\n{token}\n\nСохраните его сейчас — повторно показать не получится. Предыдущие ключи отключены.", - "API_ACCESS_GENERATED_ALERT": "Новый API ключ создан", "PRIVACY_POLICY_HEADER": "🛡️ Политика конфиденциальности", "PRIVACY_POLICY_NOT_AVAILABLE": "Политика конфиденциальности временно недоступна.", "PRIVACY_POLICY_EMPTY_ALERT": "Политика конфиденциальности ещё не заполнена.", diff --git a/app/services/web_api_token_service.py b/app/services/web_api_token_service.py index d08b6dcf..6beb06fd 100644 --- a/app/services/web_api_token_service.py +++ b/app/services/web_api_token_service.py @@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.crud import web_api_token as crud -from app.database.models import User, WebApiToken +from app.database.models import WebApiToken from app.database.universal_migration import ensure_default_web_api_token from app.utils.security import generate_api_token, hash_api_token @@ -66,14 +66,10 @@ class WebApiTokenService: expires_at: Optional[datetime] = None, created_by: Optional[str] = None, token_value: Optional[str] = None, - user: Optional[User] = None, - user_id: Optional[int] = None, ) -> Tuple[str, WebApiToken]: plain_token = token_value or generate_api_token() token_hash = self.hash_token(plain_token) - resolved_user_id = user.id if user else user_id - token = await crud.create_token( db, name=name, @@ -82,43 +78,6 @@ class WebApiTokenService: description=description, expires_at=expires_at, created_by=created_by, - user_id=resolved_user_id, - ) - - return plain_token, token - - async def issue_user_token( - self, - db: AsyncSession, - user: User, - ) -> Tuple[str, WebApiToken]: - existing_tokens = await crud.list_tokens( - db, - include_inactive=True, - user_id=user.id, - ) - - now = datetime.utcnow() - has_updates = False - for token in existing_tokens: - if token.is_active: - token.is_active = False - token.updated_at = now - has_updates = True - - if has_updates: - await db.flush() - - token_name = f"User {user.telegram_id}" - description = "Generated via Telegram bot" - created_by = f"telegram:{user.telegram_id}" - - plain_token, token = await self.create_token( - db, - name=token_name, - description=description, - created_by=created_by, - user=user, ) return plain_token, token diff --git a/app/webapi/routes/tokens.py b/app/webapi/routes/tokens.py index 55908be4..cef1fd91 100644 --- a/app/webapi/routes/tokens.py +++ b/app/webapi/routes/tokens.py @@ -1,11 +1,8 @@ from __future__ import annotations -from typing import Optional - -from fastapi import APIRouter, Depends, HTTPException, Query, Response, Security, status +from fastapi import APIRouter, Depends, HTTPException, Response, Security, status from sqlalchemy.ext.asyncio import AsyncSession -from app.database.crud.user import get_user_by_id from app.database.crud.web_api_token import ( delete_token, get_token_by_id, @@ -33,8 +30,6 @@ def _serialize(token: WebApiToken) -> TokenResponse: last_used_at=token.last_used_at, last_used_ip=token.last_used_ip, created_by=token.created_by, - user_id=token.user_id, - user_telegram_id=getattr(token.user, "telegram_id", None), ) @@ -42,13 +37,8 @@ def _serialize(token: WebApiToken) -> TokenResponse: async def get_tokens( _: WebApiToken = Security(require_api_token), db: AsyncSession = Depends(get_db_session), - user_id: Optional[int] = Query(default=None, description="Фильтр по ID пользователя"), ) -> list[TokenResponse]: - tokens = await list_tokens( - db, - include_inactive=True, - user_id=user_id, - ) + tokens = await list_tokens(db, include_inactive=True) return [_serialize(token) for token in tokens] @@ -58,19 +48,12 @@ async def create_token( actor: WebApiToken = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> TokenCreateResponse: - target_user = None - if payload.user_id is not None: - target_user = await get_user_by_id(db, payload.user_id) - if not target_user: - raise HTTPException(status.HTTP_404_NOT_FOUND, "User not found") - token_value, token = await web_api_token_service.create_token( db, name=payload.name.strip(), description=payload.description, expires_at=payload.expires_at, created_by=actor.name, - user=target_user, ) await db.commit() diff --git a/app/webapi/schemas/tokens.py b/app/webapi/schemas/tokens.py index 0019a29f..d923ab65 100644 --- a/app/webapi/schemas/tokens.py +++ b/app/webapi/schemas/tokens.py @@ -18,24 +18,12 @@ class TokenResponse(BaseModel): last_used_at: Optional[datetime] = None last_used_ip: Optional[str] = None created_by: Optional[str] = None - user_id: Optional[int] = Field( - default=None, - description="ID пользователя, которому принадлежит токен", - ) - user_telegram_id: Optional[int] = Field( - default=None, - description="Telegram ID владельца токена", - ) class TokenCreateRequest(BaseModel): name: str description: Optional[str] = None expires_at: Optional[datetime] = None - user_id: Optional[int] = Field( - default=None, - description="ID пользователя, для которого выпускается токен", - ) class TokenCreateResponse(TokenResponse): diff --git a/migrations/alembic/versions/d2a9443a9f47_add_user_link_to_web_api_tokens.py b/migrations/alembic/versions/d2a9443a9f47_add_user_link_to_web_api_tokens.py deleted file mode 100644 index 90cef86d..00000000 --- a/migrations/alembic/versions/d2a9443a9f47_add_user_link_to_web_api_tokens.py +++ /dev/null @@ -1,72 +0,0 @@ -"""add user link to web api tokens""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.engine import reflection - -revision: str = "d2a9443a9f47" -down_revision: Union[str, None] = "8fd1e338eb45" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - -TABLE_NAME = "web_api_tokens" -COLUMN_NAME = "user_id" -FK_NAME = "fk_web_api_tokens_user_id" -INDEX_NAME = "ix_web_api_tokens_user_id" - - -def _column_exists(inspector: reflection.Inspector) -> bool: - return COLUMN_NAME in [column["name"] for column in inspector.get_columns(TABLE_NAME)] - - -def _fk_exists(inspector: reflection.Inspector) -> bool: - return any(fk.get("name") == FK_NAME for fk in inspector.get_foreign_keys(TABLE_NAME)) - - -def _index_exists(inspector: reflection.Inspector) -> bool: - return any(index.get("name") == INDEX_NAME for index in inspector.get_indexes(TABLE_NAME)) - - -def upgrade() -> None: - bind = op.get_bind() - inspector = reflection.Inspector.from_engine(bind) - - if not _column_exists(inspector): - op.add_column(TABLE_NAME, sa.Column(COLUMN_NAME, sa.Integer(), nullable=True)) - inspector = reflection.Inspector.from_engine(bind) - - if bind.dialect.name in {"postgresql", "mysql"} and not _fk_exists(inspector): - try: - op.create_foreign_key( - FK_NAME, - TABLE_NAME, - "users", - [COLUMN_NAME], - ["id"], - ondelete="SET NULL", - ) - except Exception: - # Constraint creation can fail if duplicates exist; skip to keep migration resilient. - pass - - inspector = reflection.Inspector.from_engine(bind) - if not _index_exists(inspector): - op.create_index(INDEX_NAME, TABLE_NAME, [COLUMN_NAME]) - - -def downgrade() -> None: - bind = op.get_bind() - inspector = reflection.Inspector.from_engine(bind) - - if _index_exists(inspector): - op.drop_index(INDEX_NAME, table_name=TABLE_NAME) - - inspector = reflection.Inspector.from_engine(bind) - if _fk_exists(inspector): - op.drop_constraint(FK_NAME, TABLE_NAME, type_="foreignkey") - - inspector = reflection.Inspector.from_engine(bind) - if _column_exists(inspector): - op.drop_column(TABLE_NAME, COLUMN_NAME)