diff --git a/app/db/models.py b/app/db/models.py index 79aae60e5..80dc470c1 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -103,8 +103,8 @@ class User(Base): ) @hybrid_property - def reseted_usage(self): - return sum([log.used_traffic_at_reset for log in self.usage_logs]) + def reseted_usage(self) -> int: + return int(sum([log.used_traffic_at_reset for log in self.usage_logs])) @reseted_usage.expression def reseted_usage(cls): @@ -115,8 +115,8 @@ def reseted_usage(cls): ) @property - def lifetime_used_traffic(self): - return ( + def lifetime_used_traffic(self) -> int: + return int( sum([log.used_traffic_at_reset for log in self.usage_logs]) + self.used_traffic ) diff --git a/app/models/admin.py b/app/models/admin.py index 2f87908d0..640ac0128 100644 --- a/app/models/admin.py +++ b/app/models/admin.py @@ -3,7 +3,7 @@ from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from passlib.context import CryptContext -from pydantic import ConfigDict, field_validator, BaseModel +from pydantic import BaseModel, ConfigDict, field_validator from app.db import Session, crud, get_db from app.utils.jwt import get_admin_payload @@ -26,6 +26,16 @@ class Admin(BaseModel): users_usage: Optional[int] = None model_config = ConfigDict(from_attributes=True) + @field_validator("users_usage", mode='before') + def cast_to_int(cls, v): + if v is None: # Allow None values + return v + if isinstance(v, float): # Allow float to int conversion + return int(v) + if isinstance(v, int): # Allow integers directly + return v + raise ValueError("must be an integer or a float, not a string") # Reject strings + @classmethod def get_admin(cls, token: str, db: Session): payload = get_admin_payload(token) diff --git a/app/models/user.py b/app/models/user.py index b6e3dd547..fa590036c 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -77,6 +77,16 @@ class User(BaseModel): next_plan: Optional[NextPlanModel] = Field(None, nullable=True) + @field_validator('data_limit', mode='before') + def cast_to_int(cls, v): + if v is None: # Allow None values + return v + if isinstance(v, float): # Allow float to int conversion + return int(v) + if isinstance(v, int): # Allow integers directly + return v + raise ValueError("data_limit must be an integer or a float, not a string") # Reject strings + @field_validator("proxies", mode="before") def validate_proxies(cls, v, values, **kwargs): if not v: @@ -306,6 +316,16 @@ def validate_proxies(cls, v, values, **kwargs): v = {p.type: p.settings for p in v} return super().validate_proxies(v, values, **kwargs) + @field_validator("used_traffic", "lifetime_used_traffic", mode='before') + def cast_to_int(cls, v): + if v is None: # Allow None values + return v + if isinstance(v, float): # Allow float to int conversion + return int(v) + if isinstance(v, int): # Allow integers directly + return v + raise ValueError("must be an integer or a float, not a string") # Reject strings + class SubscriptionUserResponse(UserResponse): admin: Admin | None = Field(default=None, exclude=True) @@ -326,6 +346,16 @@ class UserUsageResponse(BaseModel): node_name: str used_traffic: int + @field_validator("used_traffic", mode='before') + def cast_to_int(cls, v): + if v is None: # Allow None values + return v + if isinstance(v, float): # Allow float to int conversion + return int(v) + if isinstance(v, int): # Allow integers directly + return v + raise ValueError("must be an integer or a float, not a string") # Reject strings + class UserUsagesResponse(BaseModel): username: str diff --git a/requirements.txt b/requirements.txt index be156fd2d..41a8d89fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ psutil==5.9.4 pyOpenSSL==24.2.1 PySocks==1.7.1 pyTelegramBotAPI==4.9.0 -pydantic==2.10.3 +pydantic==2.10.4 python-dateutil==2.8.2 python-decouple==3.6 python-dotenv==0.21.1