Skip to content

Commit 3195118

Browse files
committed
fix(authentication): enhance admin retrieval with metrics and refactor related functions
1 parent 23066e5 commit 3195118

File tree

3 files changed

+162
-10
lines changed

3 files changed

+162
-10
lines changed

app/routers/admin.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@
2323
from app.utils import responses
2424
from app.utils.jwt import create_admin_token
2525

26-
from .authentication import check_sudo_admin, get_current, validate_admin, validate_mini_app_admin
26+
from .authentication import (
27+
check_sudo_admin,
28+
get_current,
29+
get_current_with_metrics,
30+
validate_admin,
31+
validate_mini_app_admin,
32+
)
2733

2834
router = APIRouter(tags=["Admin"], prefix="/api/admin", responses={401: responses._401, 403: responses._403})
2935
admin_operator = AdminOperation(operator_type=OperatorType.API)
@@ -130,7 +136,7 @@ async def remove_admin(
130136

131137

132138
@router.get("", response_model=AdminDetails)
133-
def get_current_admin(admin: AdminDetails = Depends(get_current)):
139+
def get_current_admin(admin: AdminDetails = Depends(get_current_with_metrics)):
134140
"""Retrieve the current authenticated admin."""
135141
return admin
136142

app/routers/authentication.py

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
from fastapi import Depends, HTTPException, status
55
from fastapi.security import OAuth2PasswordBearer
66

7+
from sqlalchemy import func, select
78

89
from app.db import AsyncSession, get_db
910
from app.db.crud.admin import find_admins_by_telegram_id, get_admin as get_admin_by_username, get_admin_by_telegram_id
11+
from app.db.models import Admin, AdminUsageLogs, User
1012
from app.models.admin import AdminDetails, AdminValidationResult, verify_password
1113
from app.models.settings import Telegram
1214
from app.settings import telegram_settings
@@ -16,20 +18,83 @@
1618
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/admin/token")
1719

1820

21+
def _build_admin_details(
22+
db_admin: Admin,
23+
*,
24+
total_users: int = 0,
25+
reseted_usage: int | None = None,
26+
) -> AdminDetails:
27+
used_traffic = int(db_admin.used_traffic or 0)
28+
return AdminDetails(
29+
id=db_admin.id,
30+
username=db_admin.username,
31+
is_sudo=db_admin.is_sudo,
32+
total_users=int(total_users or 0),
33+
used_traffic=used_traffic,
34+
is_disabled=db_admin.is_disabled,
35+
telegram_id=db_admin.telegram_id,
36+
discord_webhook=db_admin.discord_webhook,
37+
sub_domain=db_admin.sub_domain,
38+
profile_title=db_admin.profile_title,
39+
support_url=db_admin.support_url,
40+
note=db_admin.note,
41+
notification_enable=db_admin.notification_enable,
42+
discord_id=db_admin.discord_id,
43+
sub_template=db_admin.sub_template,
44+
lifetime_used_traffic=None if reseted_usage is None else int(reseted_usage or 0) + used_traffic,
45+
)
46+
47+
48+
def _is_token_valid_for_admin(db_admin: Admin, payload: dict) -> bool:
49+
if not db_admin.password_reset_at:
50+
return True
51+
if not payload.get("created_at"):
52+
return False
53+
return db_admin.password_reset_at.astimezone(tz.utc) <= payload.get("created_at")
54+
55+
1956
async def get_admin(db: AsyncSession, token: str) -> AdminDetails | None:
2057
payload = await get_admin_payload(token)
2158
if not payload:
2259
return
2360

24-
db_admin = await get_admin_by_username(db, payload["username"], load_users=True, load_usage_logs=True)
61+
db_admin = await get_admin_by_username(db, payload["username"], load_users=False, load_usage_logs=False)
2562
if db_admin:
26-
if db_admin.password_reset_at:
27-
if not payload.get("created_at"):
28-
return
29-
if db_admin.password_reset_at.astimezone(tz.utc) > payload.get("created_at"):
30-
return
63+
if not _is_token_valid_for_admin(db_admin, payload):
64+
return
3165

32-
return AdminDetails.model_validate(db_admin)
66+
return _build_admin_details(db_admin)
67+
68+
elif payload["username"] in SUDOERS and payload["is_sudo"] is True:
69+
return AdminDetails(username=payload["username"], is_sudo=True)
70+
71+
72+
async def get_admin_with_metrics(db: AsyncSession, token: str) -> AdminDetails | None:
73+
payload = await get_admin_payload(token)
74+
if not payload:
75+
return
76+
77+
total_users_subquery = (
78+
select(func.count(User.id)).where(User.admin_id == Admin.id).correlate(Admin).scalar_subquery()
79+
)
80+
reseted_usage_subquery = (
81+
select(func.coalesce(func.sum(AdminUsageLogs.used_traffic_at_reset), 0))
82+
.where(AdminUsageLogs.admin_id == Admin.id)
83+
.correlate(Admin)
84+
.scalar_subquery()
85+
)
86+
admin_row = (
87+
await db.execute(
88+
select(Admin, total_users_subquery, reseted_usage_subquery).where(Admin.username == payload["username"])
89+
)
90+
).one_or_none()
91+
92+
if admin_row:
93+
db_admin, total_users, reseted_usage = admin_row
94+
if not _is_token_valid_for_admin(db_admin, payload):
95+
return
96+
97+
return _build_admin_details(db_admin, total_users=total_users, reseted_usage=reseted_usage)
3398

3499
elif payload["username"] in SUDOERS and payload["is_sudo"] is True:
35100
return AdminDetails(username=payload["username"], is_sudo=True)
@@ -53,6 +118,24 @@ async def get_current(db: AsyncSession = Depends(get_db), token: str = Depends(o
53118
return admin
54119

55120

121+
async def get_current_with_metrics(db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme)):
122+
admin: AdminDetails | None = await get_admin_with_metrics(db, token)
123+
if not admin:
124+
raise HTTPException(
125+
status_code=status.HTTP_401_UNAUTHORIZED,
126+
detail="Could not validate credentials",
127+
headers={"WWW-Authenticate": "Bearer"},
128+
)
129+
if admin.is_disabled:
130+
raise HTTPException(
131+
status_code=status.HTTP_403_FORBIDDEN,
132+
detail="your account has been disabled",
133+
headers={"WWW-Authenticate": "Bearer"},
134+
)
135+
136+
return admin
137+
138+
56139
async def check_sudo_admin(admin: AdminDetails = Depends(get_current)):
57140
if not admin.is_sudo:
58141
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You're not allowed")

tests/api/test_admin.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from sqlalchemy import select
1010

1111
from app.db.crud.admin import get_admin_by_telegram_id
12-
from app.db.models import Admin, NodeUserUsage
12+
from app.db.models import Admin, AdminUsageLogs, NodeUserUsage
1313
from app.models.settings import RunMethod, Telegram
1414
from app.routers.authentication import validate_mini_app_admin
1515
from tests.api import TestSession, client
@@ -93,6 +93,69 @@ def test_get_admin(access_token):
9393
assert response.json()["username"] == username
9494

9595

96+
def test_get_admin_uses_aggregate_metrics_without_loading_relationships(access_token, monkeypatch: pytest.MonkeyPatch):
97+
admin = create_admin(access_token)
98+
admin_token_response = client.post(
99+
url="/api/admin/token",
100+
data={"username": admin["username"], "password": admin["password"], "grant_type": "password"},
101+
)
102+
assert admin_token_response.status_code == status.HTTP_200_OK
103+
admin_token = admin_token_response.json()["access_token"]
104+
105+
user = create_user(admin_token, payload={"username": unique_name("admin_metric_user")})
106+
107+
async def _seed_admin_usage():
108+
async with TestSession() as session:
109+
result = await session.execute(select(Admin).where(Admin.username == admin["username"]))
110+
db_admin = result.scalar_one()
111+
db_admin.used_traffic = 12345
112+
session.add(AdminUsageLogs(admin_id=db_admin.id, used_traffic_at_reset=6789))
113+
await session.commit()
114+
115+
async def _assert_lightweight_admin_load(_, load_users: bool = True, load_usage_logs: bool = True):
116+
assert load_users is False
117+
assert load_usage_logs is False
118+
119+
try:
120+
asyncio.run(_seed_admin_usage())
121+
with monkeypatch.context() as patch_context:
122+
patch_context.setattr("app.db.crud.admin.load_admin_attrs", _assert_lightweight_admin_load)
123+
response = client.get(url="/api/admin", headers=auth_headers(admin_token))
124+
125+
assert response.status_code == status.HTTP_200_OK
126+
data = response.json()
127+
assert data["username"] == admin["username"]
128+
assert data["total_users"] == 1
129+
assert data["used_traffic"] == 12345
130+
assert data["lifetime_used_traffic"] == 19134
131+
finally:
132+
delete_user(admin_token, user["username"])
133+
delete_admin(access_token, admin["username"])
134+
135+
136+
def test_protected_routes_use_lightweight_current_admin(access_token, monkeypatch: pytest.MonkeyPatch):
137+
admin = create_admin(access_token)
138+
admin_token_response = client.post(
139+
url="/api/admin/token",
140+
data={"username": admin["username"], "password": admin["password"], "grant_type": "password"},
141+
)
142+
assert admin_token_response.status_code == status.HTTP_200_OK
143+
admin_token = admin_token_response.json()["access_token"]
144+
145+
async def _assert_lightweight_admin_load(_, load_users: bool = True, load_usage_logs: bool = True):
146+
assert load_users is False
147+
assert load_usage_logs is False
148+
149+
try:
150+
with monkeypatch.context() as patch_context:
151+
patch_context.setattr("app.db.crud.admin.load_admin_attrs", _assert_lightweight_admin_load)
152+
response = client.get(url="/api/users", headers=auth_headers(admin_token))
153+
154+
assert response.status_code == status.HTTP_200_OK
155+
finally:
156+
delete_admin(access_token, admin["username"])
157+
158+
96159
def test_admin_create(access_token):
97160
"""Test that the admin create route is accessible."""
98161

0 commit comments

Comments
 (0)