From 5d931aafe18305aeea79f1e17315ff4438659fc4 Mon Sep 17 00:00:00 2001 From: erfjab Date: Sun, 21 Dec 2025 14:07:00 +0330 Subject: [PATCH] feat[servers]: add server user access --- .../versions/20251221_server_access.py | 48 +++++++ src/db/models/__init__.py | 3 +- src/db/models/_access.py | 43 +++++++ src/db/models/_user.py | 20 ++- src/handlers/base.py | 17 ++- src/handlers/clients/create.py | 7 +- src/handlers/clients/info.py | 4 +- src/handlers/clients/menu.py | 6 +- src/handlers/clients/update.py | 17 ++- src/handlers/middlewares.py | 11 +- src/handlers/primary_ips/create.py | 11 +- src/handlers/primary_ips/info.py | 6 +- src/handlers/primary_ips/menu.py | 4 +- src/handlers/primary_ips/update.py | 6 +- src/handlers/servers/create.py | 23 +++- src/handlers/servers/info.py | 28 ++-- src/handlers/servers/menu.py | 12 +- src/handlers/servers/update.py | 120 +++++++++++++++++- src/handlers/snapshots/create.py | 9 +- src/handlers/snapshots/info.py | 6 +- src/handlers/snapshots/menu.py | 6 +- src/handlers/snapshots/update.py | 5 +- src/keys/callback.py | 3 + src/keys/manager.py | 45 ++++--- src/lang/_button.py | 3 + src/lang/_dialog.py | 12 +- src/utils/depends.py | 14 +- 27 files changed, 406 insertions(+), 83 deletions(-) create mode 100644 src/db/migrations/versions/20251221_server_access.py create mode 100644 src/db/models/_access.py diff --git a/src/db/migrations/versions/20251221_server_access.py b/src/db/migrations/versions/20251221_server_access.py new file mode 100644 index 0000000..cd050dc --- /dev/null +++ b/src/db/migrations/versions/20251221_server_access.py @@ -0,0 +1,48 @@ +"""server access + +Revision ID: 912c996d627a +Revises: 32916aca8721 +Create Date: 2025-12-21 00:23:30.783469 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "912c996d627a" +down_revision: Union[str, Sequence[str], None] = "32916aca8721" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "server_accesses", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("client_id", sa.Integer(), nullable=False), + sa.Column("server_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["client_id"], + ["clients.id"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("server_accesses") + # ### end Alembic commands ### diff --git a/src/db/models/__init__.py b/src/db/models/__init__.py index 0be8ae3..74b9c54 100644 --- a/src/db/models/__init__.py +++ b/src/db/models/__init__.py @@ -1,5 +1,6 @@ from ._user import User, UserMessage, UserState from ._client import Client +from ._access import ServerAccess -__all__ = ["Client", "User", "UserMessage", "UserState"] +__all__ = ["Client", "User", "UserMessage", "UserState", "ServerAccess"] diff --git a/src/db/models/_access.py b/src/db/models/_access.py new file mode 100644 index 0000000..9745984 --- /dev/null +++ b/src/db/models/_access.py @@ -0,0 +1,43 @@ +from sqlalchemy import BigInteger, Integer, ForeignKey +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.future import select +from sqlalchemy.ext.asyncio import AsyncSession + +from ..core import Base + + +class ServerAccess(Base): + __tablename__ = "server_accesses" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + client_id: Mapped[int] = mapped_column(Integer, ForeignKey("clients.id"), nullable=False) + server_id: Mapped[int] = mapped_column(Integer, nullable=False) + user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("users.id"), nullable=False) + + @classmethod + async def create(cls, db: AsyncSession, *, client_id: int, server_id: int, user_id: int) -> "ServerAccess": + access = cls(client_id=client_id, server_id=server_id, user_id=user_id) + db.add(access) + await db.commit() + await db.refresh(access) + return access + + @classmethod + async def get_all_by_server(cls, db: AsyncSession, client_id: int, server_id: int) -> list["ServerAccess"]: + result = await db.execute(select(cls).where(cls.client_id == client_id, cls.server_id == server_id)) + return result.scalars().all() + + @classmethod + async def get_by_user(cls, db: AsyncSession, user_id: int) -> list["ServerAccess"]: + result = await db.execute(select(cls).where(cls.user_id == user_id)) + return result.scalars().all() + + @classmethod + async def delete(cls, db: AsyncSession, client_id: int, server_id: int, user_id: int) -> None: + result = await db.execute( + select(cls).where(cls.client_id == client_id, cls.server_id == server_id, cls.user_id == user_id) + ) + access = result.scalars().first() + if access: + await db.delete(access) + await db.commit() diff --git a/src/db/models/_user.py b/src/db/models/_user.py index e07cabc..d7f70d3 100644 --- a/src/db/models/_user.py +++ b/src/db/models/_user.py @@ -1,16 +1,19 @@ import logging from datetime import datetime -from typing import Optional, Union +from typing import Optional, Union, TYPE_CHECKING from eiogram.types import User as EioUser, Message, CallbackQuery from sqlalchemy import String, BigInteger, DateTime, Integer, Text, JSON from sqlalchemy.sql import select, delete -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.hybrid import hybrid_property from src.config import BOT, TELEGRAM_ADMINS_ID from ..core import Base, GetDB +if TYPE_CHECKING: + from ._access import ServerAccess + class UserState(Base): __tablename__ = "user_states" @@ -78,8 +81,18 @@ class User(Base): join_at: Mapped[datetime] = mapped_column(DateTime(), default=datetime.now) online_at: Mapped[datetime] = mapped_column(DateTime(), default=datetime.now) + server_accesses: Mapped[list["ServerAccess"]] = relationship("ServerAccess", backref=None, lazy="selectin") + + def client_ids(self) -> set[int]: + return {access.client_id for access in self.server_accesses} + + def get_server_ids(self, client_id: Optional[int] = None) -> set[int]: + if client_id is not None: + return {access.server_id for access in self.server_accesses if access.client_id == client_id} + return {access.server_id for access in self.server_accesses} + @hybrid_property - def has_access(self) -> bool: + def is_owner(self) -> bool: return self.id in TELEGRAM_ADMINS_ID @classmethod @@ -104,6 +117,7 @@ async def upsert(cls, db: AsyncSession, *, user: EioUser) -> Optional["User"]: db.add(dbuser) logging.info(f"New user added: {dbuser.id} - {dbuser.full_name}") await db.flush() + await db.refresh(dbuser) return dbuser def __repr__(self) -> str: diff --git a/src/handlers/base.py b/src/handlers/base.py index 14d726d..3f2f94e 100644 --- a/src/handlers/base.py +++ b/src/handlers/base.py @@ -3,7 +3,7 @@ from eiogram.filters import Command, IgnoreStateFilter from eiogram.state import StateManager -from src.db import UserMessage, AsyncSession, Client +from src.db import UserMessage, AsyncSession, Client, User from src.keys import BotKB, BotCB, AreaType, TaskType from src.lang import Dialogs @@ -11,19 +11,26 @@ @router.message(Command("start"), IgnoreStateFilter()) -async def start_handler(message: Message, db: AsyncSession, state: StateManager): +async def start_handler(message: Message, db: AsyncSession, state: StateManager, dbuser: User): await state.clear_all(db=db) clients = await Client.get_all(db) - update = await message.answer(text=Dialogs.COMMAND_START, reply_markup=BotKB.home(clients=clients)) + if not dbuser.is_owner: + clients = [client for client in clients if client.id in dbuser.client_ids()] + update = await message.answer( + text=Dialogs.COMMAND_START, reply_markup=BotKB.home(clients=clients, is_owner=dbuser.is_owner) + ) return await UserMessage.clear(update) @router.callback_query(BotCB.filter(area=AreaType.HOME, task=TaskType.MENU), IgnoreStateFilter()) -async def home_menu(callback_query: CallbackQuery, db: AsyncSession, state: StateManager): +async def home_menu(callback_query: CallbackQuery, db: AsyncSession, state: StateManager, dbuser: User): await state.clear_all(db=db) + clients = await Client.get_all(db) + if not dbuser.is_owner: + clients = [client for client in clients if client.id in dbuser.client_ids()] update = await callback_query.message.answer( text=Dialogs.COMMAND_START, - reply_markup=BotKB.home(clients=clients), + reply_markup=BotKB.home(clients=clients, is_owner=dbuser.is_owner), ) return await UserMessage.clear(update) diff --git a/src/handlers/clients/create.py b/src/handlers/clients/create.py index fc3e0cc..7bcf4ab 100644 --- a/src/handlers/clients/create.py +++ b/src/handlers/clients/create.py @@ -9,6 +9,7 @@ from src.db import AsyncSession, Client, UserMessage from src.keys import BotKB, BotCB, AreaType, TaskType from src.lang import Dialogs +from src.utils.depends import ShouldBeOwner router = Router() @@ -19,13 +20,13 @@ class ClientCreateForm(StateGroup): @router.callback_query(BotCB.filter(area=AreaType.CLIENT, task=TaskType.CREATE), IgnoreStateFilter()) -async def clients_create(callback_query: CallbackQuery, db: AsyncSession, state: StateManager): +async def clients_create(callback_query: CallbackQuery, db: AsyncSession, state: StateManager, __: ShouldBeOwner): await state.upsert_context(db=db, state=ClientCreateForm.remark) return await callback_query.message.edit(text=Dialogs.CLIENTS_ENTER_REMARK, reply_markup=BotKB.home_back()) @router.message(StateFilter(ClientCreateForm.remark), Text()) -async def remark_handler(message: Message, db: AsyncSession, state: StateManager): +async def remark_handler(message: Message, db: AsyncSession, state: StateManager, __: ShouldBeOwner): if await Client.get_by_remark(db, message.text): update = await message.answer(text=Dialogs.ACTIONS_DUPLICATE, reply_markup=BotKB.home_back()) return await UserMessage.clear(update) @@ -35,7 +36,7 @@ async def remark_handler(message: Message, db: AsyncSession, state: StateManager @router.message(StateFilter(ClientCreateForm.secret), Text()) -async def secret_handler(message: Message, db: AsyncSession, state: StateManager, state_data: dict): +async def secret_handler(message: Message, db: AsyncSession, state: StateManager, state_data: dict, __: ShouldBeOwner): try: hetzner = HCloudClient(token=message.text) hetzner.datacenters.get_all() diff --git a/src/handlers/clients/info.py b/src/handlers/clients/info.py index 1a96c2b..6a74ef8 100644 --- a/src/handlers/clients/info.py +++ b/src/handlers/clients/info.py @@ -5,13 +5,13 @@ from src.db import UserMessage, Client, AsyncSession from src.lang import Dialogs from src.keys import BotKB, BotCB, AreaType, TaskType -from src.utils.depends import ClearState +from src.utils.depends import ClearState, ShouldBeOwner router = Router() @router.callback_query(BotCB.filter(area=AreaType.CLIENT, task=TaskType.INFO), IgnoreStateFilter()) -async def clients_info(callback_query: CallbackQuery, db: AsyncSession, state_data: dict, _: ClearState): +async def clients_info(callback_query: CallbackQuery, db: AsyncSession, state_data: dict, _: ClearState, __: ShouldBeOwner): client = await Client.get_by_id(db, state_data["client_id"]) if not client: return await callback_query.answer(text=Dialogs.CLIENTS_NOT_FOUND, show_alert=True) diff --git a/src/handlers/clients/menu.py b/src/handlers/clients/menu.py index f3b762b..320cb4b 100644 --- a/src/handlers/clients/menu.py +++ b/src/handlers/clients/menu.py @@ -3,7 +3,7 @@ from eiogram.filters import IgnoreStateFilter from eiogram.state import StateManager -from src.db import AsyncSession, UserMessage +from src.db import AsyncSession, UserMessage, User from src.lang import Dialogs from src.keys import BotKB, BotCB, AreaType, TaskType from src.utils.depends import ClearState @@ -13,10 +13,12 @@ @router.callback_query(BotCB.filter(area=AreaType.CLIENT, task=TaskType.MENU), IgnoreStateFilter()) async def clients_menu( - callback_query: CallbackQuery, callback_data: BotCB, db: AsyncSession, state: StateManager, _: ClearState + callback_query: CallbackQuery, callback_data: BotCB, db: AsyncSession, state: StateManager, _: ClearState, dbuser: User ): if int(callback_data.target) != 0: await state.upsert_context(db=db, client_id=callback_data.target) + if int(callback_data.target) not in dbuser.client_ids() and not dbuser.is_owner: + return await callback_query.answer(text="Access Denied", show_alert=True) update = await callback_query.message.edit( text=Dialogs.CLIENTS_MENU, reply_markup=BotKB.clients_menu(id=callback_data.target) ) diff --git a/src/handlers/clients/update.py b/src/handlers/clients/update.py index ecd2433..8134625 100644 --- a/src/handlers/clients/update.py +++ b/src/handlers/clients/update.py @@ -7,6 +7,7 @@ from src.db import AsyncSession, Client, UserMessage from src.keys import BotKB, BotCB, AreaType, TaskType, StepType from src.lang import Dialogs +from src.utils.depends import ShouldBeOwner router = Router() @@ -18,7 +19,12 @@ class ClientUpdateForm(StateGroup): @router.callback_query(BotCB.filter(area=AreaType.CLIENT, task=TaskType.UPDATE)) async def clients_update( - callback_query: CallbackQuery, callback_data: BotCB, db: AsyncSession, state: StateManager, state_data: dict + callback_query: CallbackQuery, + callback_data: BotCB, + db: AsyncSession, + state: StateManager, + state_data: dict, + __: ShouldBeOwner, ): kb = BotKB.clients_back(id=state_data["client_id"]) match callback_data.step: @@ -39,7 +45,7 @@ async def clients_update( @router.message(StateFilter(ClientUpdateForm.input), Text()) -async def input_handler(message: Message, db: AsyncSession, state: StateManager, state_data: dict): +async def input_handler(message: Message, db: AsyncSession, state: StateManager, state_data: dict, __: ShouldBeOwner): client = await Client.get_by_id(db, state_data["client_id"]) if not client: update = await message.answer(text=Dialogs.CLIENTS_NOT_FOUND, reply_markup=BotKB.home_back()) @@ -67,7 +73,12 @@ async def input_handler(message: Message, db: AsyncSession, state: StateManager, @router.callback_query(StateFilter(ClientUpdateForm.approval), BotCB.filter(area=AreaType.CLIENT, task=TaskType.UPDATE)) async def approval_handler( - callback_query: CallbackQuery, callback_data: BotCB, db: AsyncSession, state: StateManager, state_data: dict + callback_query: CallbackQuery, + callback_data: BotCB, + db: AsyncSession, + state: StateManager, + state_data: dict, + __: ShouldBeOwner, ): client = await Client.get_by_id(db, state_data["client_id"]) if not client: diff --git a/src/handlers/middlewares.py b/src/handlers/middlewares.py index a58245b..c132932 100644 --- a/src/handlers/middlewares.py +++ b/src/handlers/middlewares.py @@ -1,4 +1,3 @@ -import logging from typing import Any, Callable, Dict, Awaitable from eiogram.middleware import BaseMiddleware from eiogram.types import Update @@ -20,9 +19,11 @@ async def __call__( dbuser = await User.upsert(db, user=user) if update.message: await UserMessage.add(update.message) - if not dbuser.has_access: - logging.warning(f"User {dbuser.id} try to access admin panel") - return False data["dbuser"] = dbuser data["db"] = db - return await handler(update, data) + try: + return await handler(update, data) + except Exception as e: + if e == "Access Denied": + return + raise e diff --git a/src/handlers/primary_ips/create.py b/src/handlers/primary_ips/create.py index 0cc114c..1316622 100644 --- a/src/handlers/primary_ips/create.py +++ b/src/handlers/primary_ips/create.py @@ -6,7 +6,7 @@ from src.db import AsyncSession, UserMessage from src.keys import BotKB, BotCB, AreaType, TaskType from src.lang import Dialogs -from src.utils.depends import GetHetzner +from src.utils.depends import GetHetzner, ShouldBeOwner router = Router() @@ -17,13 +17,17 @@ class PrimaryCreateForm(StateGroup): @router.callback_query(BotCB.filter(area=AreaType.PRIMARY_IP, task=TaskType.CREATE)) -async def primary_ips_create(callback_query: CallbackQuery, callback_data: BotCB, db: AsyncSession, state: StateManager): +async def primary_ips_create( + callback_query: CallbackQuery, callback_data: BotCB, db: AsyncSession, state: StateManager, __: ShouldBeOwner +): await state.upsert_context(db=db, state=PrimaryCreateForm.remark, ip_type=callback_data.target) return await callback_query.message.edit(text=Dialogs.PRIMARY_IPS_ENTER_REMARK, reply_markup=BotKB.primary_ips_back()) @router.message(StateFilter(PrimaryCreateForm.remark), Text()) -async def remark_handler(message: Message, db: AsyncSession, state: StateManager, hetzner: GetHetzner, state_data: dict): +async def remark_handler( + message: Message, db: AsyncSession, state: StateManager, hetzner: GetHetzner, state_data: dict, __: ShouldBeOwner +): datacenters = hetzner.datacenters.get_all() if not datacenters: update = await message.answer(text=Dialogs.PRIMARY_IPS_NO_DATACENTERS) @@ -43,6 +47,7 @@ async def select_datacenter( state: StateManager, state_data: dict, hetzner: GetHetzner, + __: ShouldBeOwner, ): datacenter = hetzner.datacenters.get_by_id(int(callback_data.target)) if not datacenter: diff --git a/src/handlers/primary_ips/info.py b/src/handlers/primary_ips/info.py index 598e275..d0a078f 100644 --- a/src/handlers/primary_ips/info.py +++ b/src/handlers/primary_ips/info.py @@ -7,13 +7,15 @@ from src.db import UserMessage from src.lang import Dialogs from src.keys import BotCB, BotKB, AreaType, TaskType -from src.utils.depends import GetHetzner, ClearState +from src.utils.depends import GetHetzner, ClearState, ShouldBeOwner router = Router() @router.callback_query(BotCB.filter(area=AreaType.PRIMARY_IP, task=TaskType.INFO), IgnoreStateFilter()) -async def primary_ips_info(callback_query: CallbackQuery, callback_data: BotCB, hetzner: GetHetzner, _: ClearState): +async def primary_ips_info( + callback_query: CallbackQuery, callback_data: BotCB, hetzner: GetHetzner, _: ClearState, __: ShouldBeOwner +): primary_ip = hetzner.primary_ips.get_by_id(int(callback_data.target)) if not primary_ip: return await callback_query.message.edit(text=Dialogs.PRIMARY_IPS_NOT_FOUND) diff --git a/src/handlers/primary_ips/menu.py b/src/handlers/primary_ips/menu.py index c405b28..93f415e 100644 --- a/src/handlers/primary_ips/menu.py +++ b/src/handlers/primary_ips/menu.py @@ -5,13 +5,13 @@ from src.db import UserMessage from src.lang import Dialogs from src.keys import BotKB, BotCB, AreaType, TaskType -from src.utils.depends import GetHetzner, ClearState +from src.utils.depends import GetHetzner, ClearState, ShouldBeOwner router = Router() @router.callback_query(BotCB.filter(area=AreaType.PRIMARY_IP, task=TaskType.MENU), IgnoreStateFilter()) -async def primary_ips(callback_query: CallbackQuery, hetzner: GetHetzner, _: ClearState): +async def primary_ips(callback_query: CallbackQuery, hetzner: GetHetzner, _: ClearState, __: ShouldBeOwner, state_data: dict): primary_ips = hetzner.primary_ips.get_all() update = await callback_query.message.edit( text=Dialogs.PRIMARY_IPS_MENU, reply_markup=BotKB.primary_ips_menu(primary_ips=primary_ips) diff --git a/src/handlers/primary_ips/update.py b/src/handlers/primary_ips/update.py index 325a65a..f5c52d2 100644 --- a/src/handlers/primary_ips/update.py +++ b/src/handlers/primary_ips/update.py @@ -8,7 +8,7 @@ from src.db import AsyncSession from src.keys import BotKB, BotCB, AreaType, TaskType, StepType from src.lang import Dialogs -from src.utils.depends import GetHetzner +from src.utils.depends import GetHetzner, ShouldBeOwner router = Router() @@ -26,6 +26,7 @@ async def primary_ips_update( db: AsyncSession, state: StateManager, hetzner: GetHetzner, + __: ShouldBeOwner, ): kb = BotKB.primary_ips_back(id=callback_data.target) primary_ip = hetzner.primary_ips.get_by_id(int(callback_data.target)) @@ -62,6 +63,7 @@ async def input_handler( state: StateManager, hetzner: GetHetzner, state_data: dict, + __: ShouldBeOwner, ): primary_ip = hetzner.primary_ips.get_by_id(int(state_data["target"])) if not primary_ip: @@ -85,6 +87,7 @@ async def approval_handler( state: StateManager, hetzner: GetHetzner, state_data: dict, + __: ShouldBeOwner, ): if not callback_data.is_approve: return await callback_query.message.edit(text=Dialogs.ACTIONS_CANCELLED, reply_markup=BotKB.primary_ips_back()) @@ -122,6 +125,7 @@ async def select_handler( state: StateManager, hetzner: GetHetzner, state_data: dict, + __: ShouldBeOwner, ): primary_ip = hetzner.primary_ips.get_by_id(int(state_data["target"])) if not primary_ip: diff --git a/src/handlers/servers/create.py b/src/handlers/servers/create.py index 373039c..45fd4d6 100644 --- a/src/handlers/servers/create.py +++ b/src/handlers/servers/create.py @@ -6,7 +6,7 @@ from src.db import AsyncSession, UserMessage from src.keys import BotKB, BotCB, AreaType, TaskType from src.lang import Dialogs -from src.utils.depends import GetHetzner +from src.utils.depends import GetHetzner, ShouldBeOwner router = Router() @@ -19,13 +19,15 @@ class ServerCreateForm(StateGroup): @router.callback_query(BotCB.filter(area=AreaType.SERVER, task=TaskType.CREATE)) -async def servers_create(callback_query: CallbackQuery, db: AsyncSession, state: StateManager, state_data: dict): +async def servers_create( + callback_query: CallbackQuery, db: AsyncSession, state: StateManager, state_data: dict, __: ShouldBeOwner +): await state.upsert_context(db=db, state=ServerCreateForm.remark) return await callback_query.message.edit(text=Dialogs.SERVERS_ENTER_REMARK, reply_markup=BotKB.servers_back()) @router.message(StateFilter(ServerCreateForm.remark), Text()) -async def remark_handler(message: Message, db: AsyncSession, state: StateManager, hetzner: GetHetzner): +async def remark_handler(message: Message, db: AsyncSession, state: StateManager, hetzner: GetHetzner, __: ShouldBeOwner): if len(message.text.split(" ")) > 1: update = await message.answer(text=Dialogs.SERVERS_REMARK_VALIDATION) return await UserMessage.add(update) @@ -42,7 +44,12 @@ async def remark_handler(message: Message, db: AsyncSession, state: StateManager @router.callback_query(StateFilter(ServerCreateForm.datacenter), BotCB.filter(area=AreaType.SERVER, task=TaskType.CREATE)) async def datacenter_handler( - callback_query: CallbackQuery, callback_data: BotCB, db: AsyncSession, state: StateManager, hetzner: GetHetzner + callback_query: CallbackQuery, + callback_data: BotCB, + db: AsyncSession, + state: StateManager, + hetzner: GetHetzner, + __: ShouldBeOwner, ): plans = hetzner.server_types.get_all() if not plans: @@ -54,7 +61,12 @@ async def datacenter_handler( @router.callback_query(StateFilter(ServerCreateForm.plan), BotCB.filter(area=AreaType.SERVER, task=TaskType.CREATE)) async def plan_handler( - callback_query: CallbackQuery, callback_data: BotCB, db: AsyncSession, state: StateManager, hetzner: GetHetzner + callback_query: CallbackQuery, + callback_data: BotCB, + db: AsyncSession, + state: StateManager, + hetzner: GetHetzner, + __: ShouldBeOwner, ): plan = hetzner.server_types.get_by_id(int(callback_data.target)) if not plan: @@ -78,6 +90,7 @@ async def image_handler( state: StateManager, state_data: dict, hetzner: GetHetzner, + __: ShouldBeOwner, ): image = hetzner.images.get_by_id(int(callback_data.target)) if not image: diff --git a/src/handlers/servers/info.py b/src/handlers/servers/info.py index 061202e..7d00941 100644 --- a/src/handlers/servers/info.py +++ b/src/handlers/servers/info.py @@ -4,21 +4,33 @@ from eiogram.types import CallbackQuery from eiogram.filters import IgnoreStateFilter -from src.db import UserMessage -from src.lang import Dialogs +from src.db import UserMessage, User from src.keys import BotKB, BotCB, AreaType, TaskType from src.utils.depends import GetHetzner, ClearState from src.utils.euro import get_euro +from src.lang import Dialogs + router = Router() @router.callback_query(BotCB.filter(area=AreaType.SERVER, task=TaskType.INFO), IgnoreStateFilter()) -async def servers_info(callback_query: CallbackQuery, callback_data: BotCB, hetzner: GetHetzner, _: ClearState): - server = hetzner.servers.get_by_id(int(callback_data.target)) +async def servers_info( + callback_query: CallbackQuery, + callback_data: BotCB, + hetzner: GetHetzner, + _: ClearState, + dbuser: User, + state_data: dict, +): + server_id = int(callback_data.target) + if not dbuser.is_owner: + if server_id not in dbuser.get_server_ids(int(state_data.get("client_id"))): + return await callback_query.answer("Access Denied", show_alert=True) + + server = hetzner.servers.get_by_id(server_id) if not server: return await callback_query.message.edit(text=Dialogs.SERVERS_NOT_FOUND) - ingoing_gb = round(((server.ingoing_traffic or 0) / 1024**3), 3) outgoing_gb = round(((server.outgoing_traffic or 0) / 1024**3), 3) total_gb = round(ingoing_gb + outgoing_gb, 3) @@ -68,10 +80,10 @@ async def servers_info(callback_query: CallbackQuery, callback_data: BotCB, hetz price_hourly=price_hourly, price_monthly=price_monthly, ) - reply_markup = BotKB.servers_update(server=server) - try: - update = await callback_query.message.edit(text=text, reply_markup=reply_markup) + update = await callback_query.message.edit( + text=text, reply_markup=BotKB.servers_update(server=server, is_owner=dbuser.is_owner) + ) except Exception: await callback_query.answer() return diff --git a/src/handlers/servers/menu.py b/src/handlers/servers/menu.py index 2f259ea..1064102 100644 --- a/src/handlers/servers/menu.py +++ b/src/handlers/servers/menu.py @@ -2,7 +2,7 @@ from eiogram.types import CallbackQuery from eiogram.filters import IgnoreStateFilter -from src.db import UserMessage +from src.db import UserMessage, AsyncSession, User from src.lang import Dialogs from src.keys import BotKB, BotCB, AreaType, TaskType from src.utils.depends import GetHetzner, ClearState @@ -11,7 +11,11 @@ @router.callback_query(BotCB.filter(area=AreaType.SERVER, task=TaskType.MENU), IgnoreStateFilter()) -async def servers_menu(callback_query: CallbackQuery, hetzner: GetHetzner, _: ClearState): - servers = hetzner.servers.get_all() - update = await callback_query.message.edit(text=Dialogs.SERVERS_MENU, reply_markup=BotKB.servers_menu(servers=servers)) +async def servers_menu( + callback_query: CallbackQuery, hetzner: GetHetzner, _: ClearState, db: AsyncSession, dbuser: User, state_data: dict +): + all_servers = hetzner.servers.get_all() + if not dbuser.is_owner: + all_servers = [server for server in all_servers if server.id in dbuser.get_server_ids(int(state_data.get("client_id")))] + update = await callback_query.message.edit(text=Dialogs.SERVERS_MENU, reply_markup=BotKB.servers_menu(servers=all_servers)) return await UserMessage.clear(update, keep_current=True) diff --git a/src/handlers/servers/update.py b/src/handlers/servers/update.py index 79c681f..9eaf2b9 100644 --- a/src/handlers/servers/update.py +++ b/src/handlers/servers/update.py @@ -4,7 +4,7 @@ from eiogram.filters import StateFilter, Text from eiogram.state import StateManager, State, StateGroup -from src.db import AsyncSession, UserMessage +from src.db import AsyncSession, UserMessage, ServerAccess, User from src.keys import BotKB, BotCB, AreaType, TaskType, StepType from src.lang import Dialogs from src.utils.depends import GetHetzner @@ -20,6 +20,12 @@ class ServerUpdateForm(StateGroup): upgrade = State() +def check_access(dbuser: User, server_id: int, client_id: int) -> bool: + if dbuser.is_owner: + return True + return server_id in dbuser.get_server_ids(client_id) + + @router.callback_query(BotCB.filter(area=AreaType.SERVER, task=TaskType.UPDATE)) async def servers_update( callback_query: CallbackQuery, @@ -27,11 +33,20 @@ async def servers_update( db: AsyncSession, state: StateManager, hetzner: GetHetzner, + dbuser: User, + state_data: dict, ): kb = BotKB.servers_back(id=callback_data.target) server = hetzner.servers.get_by_id(int(callback_data.target)) if not server: return await callback_query.message.edit(text=Dialogs.SERVERS_NOT_FOUND, reply_markup=kb) + + client_id = state_data.get("client_id") + if client_id: + if not check_access(dbuser, server.id, int(client_id)): + return await callback_query.answer("Access Denied", show_alert=True) + + _state = ServerUpdateForm.input match callback_data.step: case ( StepType.SERVERS_POWER_OFF @@ -83,7 +98,6 @@ async def servers_update( kb = BotKB.images_select(images=images, task=TaskType.UPDATE, target=int(callback_data.target)) case StepType.SERVERS_REMARK: text = Dialogs.SERVERS_ENTER_REMARK - _state = ServerUpdateForm.input case StepType.SERVERS_UPGRADE: server_types = hetzner.server_types.get_all() current_type = server.server_type @@ -102,6 +116,27 @@ async def servers_update( ) _state = ServerUpdateForm.upgrade kb = BotKB.upgrade_plans_select(plans=upgrade_plans, server_id=server.id) + case StepType.SERVERS_ACCESS_GRANT: + if not dbuser.is_owner: + return await callback_query.answer("Access Denied", show_alert=True) + text = Dialogs.SERVERS_ACCESS_GRANT_PROMPT + case StepType.SERVERS_ACCESS_REVOKE: + if not dbuser.is_owner: + return await callback_query.answer("Access Denied", show_alert=True) + text = Dialogs.SERVERS_ACCESS_REVOKE_PROMPT + case StepType.SERVERS_ACCESS_LIST: + if not dbuser.is_owner: + return await callback_query.answer("Access Denied", show_alert=True) + server_id = int(callback_data.target) + accesses = await ServerAccess.get_all_by_server(db, int(state_data.get("client_id")), server_id) + if not accesses: + list_text = "No access granted." + else: + list_text = "\n".join([f"• {a.user_id}" for a in accesses]) + return await callback_query.message.edit( + text=Dialogs.SERVERS_ACCESS_LIST.format(list=list_text), reply_markup=BotKB.servers_back(server_id) + ) + case _: return await callback_query.answer(text="Invalid step!", show_alert=True) await state.upsert_context(db=db, state=_state, step=callback_data.step, target=callback_data.target) @@ -114,7 +149,19 @@ async def select_image_handler( callback_data: BotCB, db: AsyncSession, state: StateManager, + dbuser: User, + state_data: dict, + hetzner: GetHetzner, ): + server = hetzner.servers.get_by_id(int(state_data["target"])) + if not server: + return await callback_query.answer(text=Dialogs.SERVERS_NOT_FOUND, show_alert=True) + + client_id = state_data.get("client_id") + if client_id: + if not check_access(dbuser, server.id, int(client_id)): + return await callback_query.answer("Access Denied", show_alert=True) + await state.upsert_context(db=db, state=ServerUpdateForm.approval, image_id=callback_data.target) return await callback_query.message.edit( text=Dialogs.ACTIONS_CONFIRM, @@ -123,18 +170,65 @@ async def select_image_handler( @router.message(StateFilter(ServerUpdateForm.input), Text()) -async def input_handler(message: Message, state: StateManager, db: StateFilter, state_data: dict, hetzner: GetHetzner): +async def input_handler( + message: Message, state: StateManager, db: StateFilter, state_data: dict, hetzner: GetHetzner, dbuser: User +): server = hetzner.servers.get_by_id(int(state_data["target"])) if not server: update = await message.answer(text=Dialogs.SERVERS_NOT_FOUND, reply_markup=BotKB.servers_back()) return await UserMessage.add(update) + client_id = state_data.get("client_id") + if client_id: + if not check_access(dbuser, server.id, int(client_id)): + update = await message.answer("Access Denied") + return await UserMessage.add(update) + match state_data["step"]: case StepType.SERVERS_REMARK: if len(message.text.split(" ")) > 1: update = await message.answer(text=Dialogs.SERVERS_REMARK_VALIDATION) return await UserMessage.add(update) server.update(name=message.text) + case StepType.SERVERS_ACCESS_GRANT: + if not client_id: + update = await message.answer("Client ID missing from state.", reply_markup=BotKB.home_back()) + return await UserMessage.clear(update) + + if not message.text.isdigit(): + update = await message.answer("Invalid User ID. Please enter a number.") + return await UserMessage.add(update) + + user_id = int(message.text) + user = await User.get_by_id(db, user_id) + if not user: + update = await message.answer("User not found in database. The user must start the bot first.") + return await UserMessage.add(update) + + existing = await ServerAccess.get_all_by_server(db, int(client_id), server.id) + if any(a.user_id == user_id for a in existing): + update = await message.answer(Dialogs.SERVERS_ACCESS_ALREADY_EXISTS.format(chat_id=user_id)) + return await UserMessage.add(update) + else: + await ServerAccess.create(db, client_id=int(client_id), server_id=server.id, user_id=user_id) + update = await message.answer(Dialogs.SERVERS_ACCESS_GRANTED.format(chat_id=user_id)) + return await UserMessage.add(update) + case StepType.SERVERS_ACCESS_REVOKE: + if not client_id: + update = await message.answer("Client ID missing from state.", reply_markup=BotKB.home_back()) + return await UserMessage.clear(update) + if not message.text.isdigit(): + update = await message.answer("Invalid User ID.") + return await UserMessage.add(update) + user_id = int(message.text) + existing = await ServerAccess.get_all_by_server(db, int(client_id), server.id) + if not any(a.user_id == user_id for a in existing): + update = await message.answer(Dialogs.SERVERS_ACCESS_NOT_FOUND.format(chat_id=user_id)) + return await UserMessage.add(update) + else: + await ServerAccess.delete(db, int(client_id), server.id, user_id) + update = await message.answer(Dialogs.SERVERS_ACCESS_REVOKED.format(chat_id=user_id)) + return await UserMessage.add(update) await state.clear_state(db=db) update = await message.answer(text=Dialogs.ACTIONS_SUCCESS, reply_markup=BotKB.servers_back(server.id)) @@ -149,6 +243,7 @@ async def select_ip_handler( state: StateManager, hetzner: GetHetzner, state_data: dict, + dbuser: User, ): primary_ip = hetzner.primary_ips.get_by_id(int(callback_data.target)) if not primary_ip: @@ -157,6 +252,11 @@ async def select_ip_handler( if not server: return await callback_query.answer(text=Dialogs.SERVERS_NOT_FOUND, show_alert=True) + client_id = state_data.get("client_id") + if client_id: + if not check_access(dbuser, server.id, int(client_id)): + return await callback_query.answer("Access Denied", show_alert=True) + await callback_query.message.edit(text=Dialogs.ACTIONS_WAITING) if server.status != "off": server.power_off() @@ -175,6 +275,7 @@ async def select_upgrade_handler( state: StateManager, hetzner: GetHetzner, state_data: dict, + dbuser: User, ): server_type = hetzner.server_types.get_by_id(int(callback_data.target)) if not server_type: @@ -183,6 +284,11 @@ async def select_upgrade_handler( if not server: return await callback_query.answer(text=Dialogs.SERVERS_NOT_FOUND, show_alert=True) + client_id = state_data.get("client_id") + if client_id: + if not check_access(dbuser, server.id, int(client_id)): + return await callback_query.answer("Access Denied", show_alert=True) + if server.status != "off": return await callback_query.answer(text=Dialogs.SERVERS_SHOULD_BE_OFF, show_alert=True) await callback_query.message.edit(text=Dialogs.ACTIONS_WAITING) @@ -199,13 +305,19 @@ async def approval_handler( state: StateManager, state_data: dict, hetzner: GetHetzner, + dbuser: User, ): if not callback_data.is_approve: return await callback_query.message.edit(text=Dialogs.ACTIONS_CANCELLED, reply_markup=BotKB.servers_back()) server = hetzner.servers.get_by_id(int(state_data["target"])) if not server: - return await callback_query.message.edit(text=Dialogs.SERVERS_NOT_FOUND, reply_markup=BotKB.home_back()) + return await callback_query.answer(text=Dialogs.SERVERS_NOT_FOUND, show_alert=True) + + client_id = state_data.get("client_id") + if client_id: + if not check_access(dbuser, server.id, int(client_id)): + return await callback_query.answer("Access Denied", show_alert=True) kb = BotKB.servers_back(id=server.id) match state_data["step"]: diff --git a/src/handlers/snapshots/create.py b/src/handlers/snapshots/create.py index 07b2d2b..0db9494 100644 --- a/src/handlers/snapshots/create.py +++ b/src/handlers/snapshots/create.py @@ -6,7 +6,7 @@ from src.db import AsyncSession, UserMessage from src.keys import BotKB, BotCB, AreaType, TaskType from src.lang import Dialogs -from src.utils.depends import GetHetzner +from src.utils.depends import GetHetzner, ShouldBeOwner router = Router() @@ -17,7 +17,9 @@ class SnapshotCreateForm(StateGroup): @router.callback_query(BotCB.filter(area=AreaType.SNAPSHOT, task=TaskType.CREATE)) -async def snapshots_create(callback_query: CallbackQuery, db: AsyncSession, state: StateManager, hetzner: GetHetzner): +async def snapshots_create( + callback_query: CallbackQuery, db: AsyncSession, state: StateManager, hetzner: GetHetzner, __: ShouldBeOwner +): servers = hetzner.servers.get_all() if not servers: return await callback_query.answer(text=Dialogs.SNAPSHOTS_SERVERS_NOT_FOUND, show_alert=True) @@ -26,7 +28,7 @@ async def snapshots_create(callback_query: CallbackQuery, db: AsyncSession, stat @router.message(StateFilter(SnapshotCreateForm.remark), Text()) -async def remark_handler(message: Message, db: AsyncSession, state: StateManager, hetzner: GetHetzner): +async def remark_handler(message: Message, db: AsyncSession, state: StateManager, hetzner: GetHetzner, __: ShouldBeOwner): servers = hetzner.servers.get_all() if not servers: update = await message.answer(text=Dialogs.SNAPSHOTS_SERVERS_NOT_FOUND) @@ -47,6 +49,7 @@ async def server_handler( state: StateManager, hetzner: GetHetzner, state_data: dict, + __: ShouldBeOwner, ): server = hetzner.servers.get_by_id(int(callback_data.target)) if not server: diff --git a/src/handlers/snapshots/info.py b/src/handlers/snapshots/info.py index 7d9db9f..7f86ca1 100644 --- a/src/handlers/snapshots/info.py +++ b/src/handlers/snapshots/info.py @@ -7,13 +7,15 @@ from src.db import UserMessage from src.lang import Dialogs from src.keys import BotKB, BotCB, AreaType, TaskType -from src.utils.depends import GetHetzner, ClearState +from src.utils.depends import GetHetzner, ClearState, ShouldBeOwner router = Router() @router.callback_query(BotCB.filter(area=AreaType.SNAPSHOT, task=TaskType.INFO), IgnoreStateFilter()) -async def snapshots_info(callback_query: CallbackQuery, callback_data: BotCB, hetzner: GetHetzner, _: ClearState): +async def snapshots_info( + callback_query: CallbackQuery, callback_data: BotCB, hetzner: GetHetzner, _: ClearState, __: ShouldBeOwner +): snapshot = hetzner.images.get_by_id(int(callback_data.target)) if not snapshot: return await callback_query.message.edit(text=Dialogs.SNAPSHOTS_NOT_FOUND) diff --git a/src/handlers/snapshots/menu.py b/src/handlers/snapshots/menu.py index 6c08da8..b6d8cd0 100644 --- a/src/handlers/snapshots/menu.py +++ b/src/handlers/snapshots/menu.py @@ -5,13 +5,15 @@ from src.db import UserMessage from src.lang import Dialogs from src.keys import BotKB, BotCB, AreaType, TaskType -from src.utils.depends import GetHetzner, ClearState +from src.utils.depends import GetHetzner, ClearState, ShouldBeOwner router = Router() @router.callback_query(BotCB.filter(area=AreaType.SNAPSHOT, task=TaskType.MENU), IgnoreStateFilter()) -async def snapshots_menu(callback_query: CallbackQuery, hetzner: GetHetzner, _: ClearState): +async def snapshots_menu( + callback_query: CallbackQuery, hetzner: GetHetzner, _: ClearState, __: ShouldBeOwner, state_data: dict +): snapshots = hetzner.images.get_all(type="snapshot") update = await callback_query.message.edit( text=Dialogs.SNAPSHOTS_MENU, reply_markup=BotKB.snapshots_menu(snapshots=snapshots) diff --git a/src/handlers/snapshots/update.py b/src/handlers/snapshots/update.py index 619505b..5527434 100644 --- a/src/handlers/snapshots/update.py +++ b/src/handlers/snapshots/update.py @@ -6,7 +6,7 @@ from src.db import AsyncSession from src.keys import BotKB, BotCB, AreaType, TaskType, StepType from src.lang import Dialogs -from src.utils.depends import GetHetzner +from src.utils.depends import GetHetzner, ShouldBeOwner router = Router() @@ -24,6 +24,7 @@ async def snapshots_update( db: AsyncSession, state: StateManager, hetzner: GetHetzner, + __: ShouldBeOwner, ): kb = BotKB.snapshots_back(id=callback_data.target) snapshot = hetzner.images.get_by_id(int(callback_data.target)) @@ -50,6 +51,7 @@ async def input_handler( state: StateManager, hetzner: GetHetzner, state_data: dict, + __: ShouldBeOwner, ): snapshot = hetzner.images.get_by_id(int(state_data["target"])) if not snapshot: @@ -73,6 +75,7 @@ async def approval_handler( state: StateManager, hetzner: GetHetzner, state_data: dict, + __: ShouldBeOwner, ): if not callback_data.is_approve: return await callback_query.message.edit(text=Dialogs.ACTIONS_CANCELLED, reply_markup=BotKB.snapshots_back()) diff --git a/src/keys/callback.py b/src/keys/callback.py index 05ab217..75206aa 100644 --- a/src/keys/callback.py +++ b/src/keys/callback.py @@ -37,6 +37,9 @@ class StepType(StrEnum): SERVERS_ASSIGN_IPV4 = "saa4" SERVERS_ASSIGN_IPV6 = "saa6" SERVERS_UPGRADE = "sup" + SERVERS_ACCESS_GRANT = "sag" + SERVERS_ACCESS_LIST = "sal" + SERVERS_ACCESS_REVOKE = "sar" SNAPSHOTS_RESTORE = "srs" SNAPSHOTS_DELETE = "sds" SNAPSHOTS_REMARK = "srm" diff --git a/src/keys/manager.py b/src/keys/manager.py index 8f8e8e9..9de126c 100644 --- a/src/keys/manager.py +++ b/src/keys/manager.py @@ -28,7 +28,7 @@ def _back(cls, *, kb: InlineKeyboardBuilder, area: AreaType = AreaType.HOME, tar ) @classmethod - def home(cls, *, clients: List[Client]) -> InlineKeyboardMarkup: + def home(cls, *, clients: List[Client], is_owner: bool = False) -> InlineKeyboardMarkup: kb = InlineKeyboardBuilder() for client in clients: kb.add( @@ -36,12 +36,13 @@ def home(cls, *, clients: List[Client]) -> InlineKeyboardMarkup: callback_data=BotCB(area=AreaType.CLIENT, task=TaskType.MENU, target=client.id).pack(), ) kb.adjust(2) - kb.row( - InlineKeyboardButton( - text=Buttons.CLIENTS_CREATE, callback_data=BotCB(area=AreaType.CLIENT, task=TaskType.CREATE).pack() - ), - size=1, - ) + if is_owner: + kb.row( + InlineKeyboardButton( + text=Buttons.CLIENTS_CREATE, callback_data=BotCB(area=AreaType.CLIENT, task=TaskType.CREATE).pack() + ), + size=1, + ) kb.row(InlineKeyboardButton(text=Buttons.OWNER, url="https://t.me/erfjabs"), size=1) return kb.as_markup() @@ -161,25 +162,35 @@ def servers_menu(cls, servers: List[Server]) -> InlineKeyboardMarkup: return kb.as_markup() @classmethod - def servers_update(cls, server: Server) -> InlineKeyboardMarkup: + def servers_update(cls, server: Server, is_owner: bool = False) -> InlineKeyboardMarkup: kb = InlineKeyboardBuilder() update = { StepType.SERVERS_REMARK: Buttons.SERVERS_REMARK, - StepType.SERVERS_UPGRADE: Buttons.SERVERS_UPGRADE, StepType.SERVERS_POWER_OFF: Buttons.SERVERS_POWER_OFF, StepType.SERVERS_POWER_ON: Buttons.SERVERS_POWER_ON, - StepType.SERVERS_CREATE_SNAPSHOT: Buttons.SERVERS_CREATE_SNAPSHOT, StepType.SERVERS_REBOOT: Buttons.SERVERS_REBOOT, StepType.SERVERS_REBUILD: Buttons.SERVERS_REBUILD, - StepType.SERVERS_DEL_SNAPSHOT: Buttons.SERVERS_DEL_SNAPSHOT, StepType.SERVERS_RESET_PASSWORD: Buttons.SERVERS_RESET_PASSWORD, StepType.SERVERS_RESET: Buttons.SERVERS_RESET, - StepType.SERVERS_REMOVE: Buttons.SERVERS_REMOVE, - StepType.SERVERS_UNASSIGN_IPV4: Buttons.SERVERS_UNASSIGN_IPV4, - StepType.SERVERS_UNASSIGN_IPV6: Buttons.SERVERS_UNASSIGN_IPV6, - StepType.SERVERS_ASSIGN_IPV4: Buttons.SERVERS_ASSIGN_IPV4, - StepType.SERVERS_ASSIGN_IPV6: Buttons.SERVERS_ASSIGN_IPV6, } + + if is_owner: + update.update( + { + StepType.SERVERS_CREATE_SNAPSHOT: Buttons.SERVERS_CREATE_SNAPSHOT, + StepType.SERVERS_UPGRADE: Buttons.SERVERS_UPGRADE, + StepType.SERVERS_DEL_SNAPSHOT: Buttons.SERVERS_DEL_SNAPSHOT, + StepType.SERVERS_UNASSIGN_IPV4: Buttons.SERVERS_UNASSIGN_IPV4, + StepType.SERVERS_UNASSIGN_IPV6: Buttons.SERVERS_UNASSIGN_IPV6, + StepType.SERVERS_ASSIGN_IPV4: Buttons.SERVERS_ASSIGN_IPV4, + StepType.SERVERS_ASSIGN_IPV6: Buttons.SERVERS_ASSIGN_IPV6, + StepType.SERVERS_ACCESS_GRANT: Buttons.SERVERS_ACCESS_GRANT, + StepType.SERVERS_ACCESS_LIST: Buttons.SERVERS_ACCESS_LIST, + StepType.SERVERS_ACCESS_REVOKE: Buttons.SERVERS_ACCESS_REVOKE, + StepType.SERVERS_REMOVE: Buttons.SERVERS_REMOVE, + } + ) + for step, button in update.items(): kb.add( text=button, @@ -190,7 +201,7 @@ def servers_update(cls, server: Server) -> InlineKeyboardMarkup: step=step, ).pack(), ) - kb.adjust(1, 1, 2, 1, 2, 1, 2, 1, 2, 2) + kb.adjust(2) kb.row( InlineKeyboardButton( text=Buttons.SERVERS_REFRESH, diff --git a/src/lang/_button.py b/src/lang/_button.py index 82ae93f..705400d 100644 --- a/src/lang/_button.py +++ b/src/lang/_button.py @@ -34,6 +34,9 @@ class Buttons(StrEnum): SERVERS_UNASSIGN_IPV6 = "āŒ Unassign IPv6" SERVERS_UPGRADE = "ā¬†ļø Upgrade" SERVERS_REFRESH = "šŸ”„ Refresh" + SERVERS_ACCESS_GRANT = "āž• Grant Access" + SERVERS_ACCESS_LIST = "šŸ“‹ List Access" + SERVERS_ACCESS_REVOKE = "āŒ Revoke Access" ### Snapshots SNAPSHOTS = "šŸ“ø Snapshots" diff --git a/src/lang/_dialog.py b/src/lang/_dialog.py index e2c8366..0e72a05 100644 --- a/src/lang/_dialog.py +++ b/src/lang/_dialog.py @@ -3,10 +3,7 @@ class Dialogs(StrEnum): ### Commands - COMMAND_START = """ -🌟 Welcome! I'm your Server Management Assistant -šŸ’ Project Sponsor: PingiHost -""" + COMMAND_START = "🌟 Welcome! I'm your Server Management Assistant" ### Actions ACTIONS_SUCCESS = "šŸŽ‰āœ… Action completed successfully." @@ -28,6 +25,13 @@ class Dialogs(StrEnum): ### Servers SERVERS_MENU = "šŸ–„ļø Servers Menu\nšŸ‘‡ Select an action from the menu below." SERVERS_NOT_FOUND = "šŸ”āŒ Not found server." + SERVERS_ACCESS_GRANT_PROMPT = "āœļø Enter the Chat ID to grant access:" + SERVERS_ACCESS_REVOKE_PROMPT = "āœļø Enter the Chat ID to revoke access:" + SERVERS_ACCESS_LIST = "šŸ“‹ Access List:\n{list}" + SERVERS_ACCESS_GRANTED = "šŸŽ‰āœ… Access granted to {chat_id}." + SERVERS_ACCESS_REVOKED = "šŸŽ‰āœ… Access revoked from {chat_id}." + SERVERS_ACCESS_ALREADY_EXISTS = "āš ļøāŒ Access already exists for {chat_id}." + SERVERS_ACCESS_NOT_FOUND = "āš ļøāŒ Access not found for {chat_id}." SERVERS_INFO = """ šŸš€ Name: {name} [{status}] šŸ”— IPV4: {ipv4} diff --git a/src/utils/depends.py b/src/utils/depends.py index 161f22d..2e313d9 100644 --- a/src/utils/depends.py +++ b/src/utils/depends.py @@ -2,10 +2,11 @@ from typing import Optional, Annotated from eiogram.state import StateManager +from eiogram.types import Update from eiogram.utils.depends import Depends from hcloud import Client as hcloud_client -from src.db import AsyncSession, Client +from src.db import AsyncSession, Client, User, UserMessage async def clear_state(db: AsyncSession, state: StateManager) -> None: @@ -24,5 +25,16 @@ async def get_hetzner(db: AsyncSession, state_data: dict) -> Optional[hcloud_cli return hcloud_client(token=client.secret) +async def should_be_owner(update: Update, dbuser: User, db: AsyncSession) -> None: + if not dbuser.is_owner: + if update.callback_query: + await update.callback_query.answer("Access Denied", show_alert=True) + elif update.message: + _update = await update.message.answer("Access Denied", reply_markup=None) + await UserMessage.clear(_update) + raise Exception("Access Denied") + + GetHetzner = Annotated[Optional[hcloud_client], Depends(get_hetzner)] ClearState = Annotated[None, Depends(clear_state)] +ShouldBeOwner = Annotated[None, Depends(should_be_owner)]