Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bfe0a7d
fix(configs-qrcode-modal): update fetch URL to use base64 links and d…
MatinDehghanian Feb 24, 2026
2ba918a
feat(telegram): implement NATS-backed memory storage for FSM synchron…
ImMohammad20000 Feb 24, 2026
572124a
feat(user-modal): integrate dropdown menu for user actions and stream…
x0sina Feb 24, 2026
537223e
feat(advance-search): add 'Show created by' toggle for sudo users and…
x0sina Feb 24, 2026
5b6c9fd
fix(locales): add 'Show created by' translation for multiple language…
x0sina Feb 24, 2026
fbabdf4
fix(admins): improve error handling in admin status toggle and simpli…
x0sina Feb 24, 2026
c89b5ff
feat(admins): prevent sudo admins from disabling their own accounts a…
x0sina Feb 24, 2026
e4955c6
fix(user-modal): enhance actions menu with improved dropdown interact…
x0sina Feb 24, 2026
33c6ce8
fix(user-modal): normalize data limit handling and adjust reset strat…
x0sina Feb 24, 2026
cfc7b21
fix(users-statistics): adjust layout for total users card to improve …
x0sina Feb 24, 2026
acdc56d
fix(subscription-modal): adjust layout and sizing for QR code and con…
x0sina Feb 24, 2026
c6741cb
fix(dashboard-statistics): update layout for memory and disk usage di…
x0sina Feb 24, 2026
273a890
feat(chart-view): implement area chart option and enhance chart view …
x0sina Feb 24, 2026
cf60574
refactor(sidebar): replace panel icons with chevrons and enhance butt…
x0sina Feb 24, 2026
1889af0
feat(subscription-modal): implement fetch links with timeout fallback…
x0sina Feb 24, 2026
c2284d3
fix(user-templates): add onRowClick handler for edit functionality in…
x0sina Feb 25, 2026
5ca5119
fix(user-templates): enhance edit functionality with onClick handler …
x0sina Feb 25, 2026
d6b2eb6
fix: validate usename length for bulk users
ImMohammad20000 Feb 25, 2026
4e5aff2
fix: validate base username befor generating usernames
ImMohammad20000 Feb 25, 2026
70252a8
fix(node-manager): change shutdown process to await completion after …
x0sina Feb 25, 2026
51e83be
fix(system-statistics): improve layout and styling of memory usage di…
x0sina Feb 25, 2026
771accf
fix(user-operation): invert username length validation condition to e…
x0sina Feb 25, 2026
83b3fa6
fix(dashboard-statistics): adjust font sizes for traffic value displa…
x0sina Feb 25, 2026
4c0e0df
feat(core-config-modal): integrate Ace Editor for JSON editing in mob…
x0sina Feb 25, 2026
00e672d
fix: install_service.sh run main script inside uv .venv environment (…
qmel Feb 25, 2026
ae8ca34
fix(ci): refactor build-dev.yml for improved Docker handling
x0sina Feb 27, 2026
241febf
fix(ci): Docker-dev build workflow for improvements
x0sina Feb 27, 2026
44f5556
Merge branch 'dev-matt' of https://github.com/MatinDehghanian/pasarga…
MatinDehghanian Feb 27, 2026
1fe6b1c
fix(dashboard): use ClipboardItem to fix config copy in Safari
MatinDehghanian Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .github/workflows/build-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ name: Build-Docker-dev

on:
push:
branches:
- "dev"
- "next"
- "nats-io"

branches-ignore:
- main

permissions:
contents: read
packages: write
Expand Down
2 changes: 1 addition & 1 deletion app/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def validate_username(cls, v):
# Skip validation if username is None (for random strategy)
if v is None:
return v
return UserValidator.validate_username(v)
return UserValidator.validate_username(username=v, len_check=False)

@model_validator(mode="after")
def validate_username_strategy(self):
Expand Down
4 changes: 2 additions & 2 deletions app/node/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ async def update_node(self, node: Node, max_message_size: int | None = None) ->

self._nodes[node.id] = new_node

# Stop the old node in the background so we don't block callers.
asyncio.create_task(self._shutdown_node(old_node))
# Stop the old node after releasing the lock.
await self._shutdown_node(old_node)

return new_node

Expand Down
3 changes: 3 additions & 0 deletions app/operation/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ async def modify_admin(
message="You're not allowed to modify sudoer's account. Use pasarguard cli / tui instead.", code=403
)

if db_admin.username == current_admin.username and modified_admin.is_disabled is True:
await self.raise_error(message="You're not allowed to disable your own account.", code=403)

if modified_admin.telegram_id is not None:
existing_admins = await find_admins_by_telegram_id(
db, modified_admin.telegram_id, exclude_admin_id=db_admin.id, limit=1
Expand Down
62 changes: 45 additions & 17 deletions app/operation/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,18 @@
ModifyUserByTemplate,
RemoveUsersResponse,
UserCreate,
UserSimple,
UsersSimpleResponse,
UserModify,
UsernameGenerationStrategy,
UserNotificationResponse,
UserResponse,
UserSimple,
UsersResponse,
UsersSimpleResponse,
UserSubscriptionUpdateChart,
UserSubscriptionUpdateChartSegment,
UserSubscriptionUpdateList,
)
from app.node.sync import remove_user as sync_remove_user
from app.node.sync import sync_user, sync_users
from app.node.sync import remove_user as sync_remove_user, sync_user, sync_users
from app.operation import BaseOperation, OperatorType
from app.settings import subscription_settings
from app.utils.jwt import create_subscription_token
Expand All @@ -83,9 +82,9 @@ def _is_non_blocking_sync_operator(operator_type: OperatorType) -> bool:

@staticmethod
def _format_validation_errors(error: ValidationError) -> str:
return "; ".join(
[f"{'.'.join(str(loc_part) for loc_part in err['loc'])}: {err['msg']}" for err in error.errors()]
)
return "; ".join([
f"{'.'.join(str(loc_part) for loc_part in err['loc'])}: {err['msg']}" for err in error.errors()
])

@staticmethod
async def generate_subscription_url(user: UserNotificationResponse):
Expand All @@ -105,7 +104,14 @@ async def _generate_usernames(
count: int,
strategy: UsernameGenerationStrategy,
start_number: int | None = None,
username_prefix: str | None = None,
username_suffix: str | None = None,
) -> list[str]:
def _apply_affixes(candidate: str) -> str:
return (
f"{username_prefix if username_prefix else ''}{candidate}{username_suffix if username_suffix else ''}"
)

if count <= 0:
await self.raise_error(message="count must be greater than zero", code=400)
if start_number is not None and start_number < 0:
Expand All @@ -125,7 +131,7 @@ async def _generate_usernames(
attempts += 1
if attempts > max_attempts:
await self.raise_error(message="unable to generate unique usernames", code=500)
candidate = secrets.token_hex(6)
candidate = _apply_affixes(secrets.token_hex(6))
if candidate in seen:
continue
seen.add(candidate)
Expand All @@ -136,7 +142,13 @@ async def _generate_usernames(
if not base_username:
await self.raise_error(message="base username is required for sequence strategy", code=400)

prefix = base_username
sequence_base_username = _apply_affixes(base_username)

if not (3 <= len(sequence_base_username) <= 128):
await self.raise_error(
message="base username with affixes must be between 3 and 128 characters", code=400
)

width = 0
inferred_start_number = 1

Expand All @@ -146,7 +158,7 @@ async def _generate_usernames(
suffix = str(current)
if width:
suffix = suffix.zfill(width)
generated.append(f"{prefix}{suffix}")
generated.append(f"{sequence_base_username}{suffix}")
current += 1
return generated

Expand Down Expand Up @@ -620,16 +632,26 @@ def apply_settings(user_args: UserCreate | UserModify, template: UserTemplate) -

return user_args

def _build_user_create_from_template(
self, user_template: UserTemplate, payload: CreateUserFromTemplate
) -> UserCreate:
new_user_args = self.load_base_user_args(user_template)
new_user_args["username"] = (
@staticmethod
def _apply_template_username_affixes(username: str, user_template: UserTemplate) -> str:
return (
f"{user_template.username_prefix if user_template.username_prefix else ''}"
f"{payload.username}"
f"{username}"
f"{user_template.username_suffix if user_template.username_suffix else ''}"
)

def _build_user_create_from_template(
self,
user_template: UserTemplate,
payload: CreateUserFromTemplate,
apply_template_username_affixes: bool = True,
) -> UserCreate:
new_user_args = self.load_base_user_args(user_template)
username = payload.username
if apply_template_username_affixes:
username = self._apply_template_username_affixes(username, user_template)
new_user_args["username"] = username

try:
new_user = UserCreate(**new_user_args, note=payload.note)
except ValidationError as e:
Expand Down Expand Up @@ -711,6 +733,8 @@ async def bulk_create_users_from_template(
count=bulk_users.count,
strategy=bulk_users.strategy,
start_number=bulk_users.start_number,
username_prefix=user_template.username_prefix,
username_suffix=user_template.username_suffix,
)

def builder(username: str):
Expand All @@ -719,7 +743,11 @@ def builder(username: str):
user_template_id=template_payload.user_template_id,
note=template_payload.note,
)
return self._build_user_create_from_template(user_template, payload)
return self._build_user_create_from_template(
user_template,
payload,
apply_template_username_affixes=False,
)

users_to_create = self._build_bulk_user_models(candidate_usernames, builder)

Expand Down
19 changes: 16 additions & 3 deletions app/telegram/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.enums import ParseMode
from aiogram.exceptions import TelegramBadRequest, TelegramNetworkError, TelegramRetryAfter, TelegramUnauthorizedError
from aiogram.fsm.storage.memory import MemoryStorage
from nats.js.kv import KeyValue
from python_socks._errors import ProxyConnectionError

from app import on_shutdown, on_startup
from app.models.settings import RunMethod, Telegram
from app.settings import telegram_settings
from app.utils.logger import get_logger
from app.nats import is_nats_enabled
from app.nats.client import setup_nats_kv
from app.settings import telegram_settings
from app.utils.logger import get_logger
from config import NATS_TELEGRAM_KV_BUCKET

from .fsm_storage import NatsFSMStorage
from .handlers import include_routers
from .middlewares import setup_middlewares

Expand All @@ -29,14 +31,21 @@ def __init__(self):
self._bot: Bot | None = None
self._polling_task: asyncio.Task | None = None
self._lock = Lock()
self._dp = Dispatcher()
self._dp = self._create_dispatcher()
self._handlers_registered = False
self._shutdown_in_progress = False
self._stop_requested = False
self._settings_key: tuple | None = None
self._kv: KeyValue | None = None
self._nats_conn = None

@staticmethod
def _create_dispatcher() -> Dispatcher:
if is_nats_enabled():
storage = NatsFSMStorage(NATS_TELEGRAM_KV_BUCKET)
return Dispatcher(storage=storage, events_isolation=storage.create_isolation())
return Dispatcher(storage=MemoryStorage())

def get_bot(self) -> Bot | None:
return self._bot

Expand Down Expand Up @@ -132,6 +141,10 @@ async def shutdown(self):
async with self._lock:
self._stop_requested = True
await self._shutdown_locked()
try:
await self._dp.fsm.close()
except Exception:
pass
# Close NATS KV connection if one was opened
if self._nats_conn:
try:
Expand Down
Loading