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)]