Files
Marzban/app/models/admin.py
Mohammad ea6a3d2eb6 Migrate to Pydantic V2 (#1495)
* chore: update pydantic to version 2.10.2 and refactor model validators

* refactor: simplify field validators by removing unnecessary pre and always flags

* remove typing_extensions==4.9.0 from requirements.txt

* refactor: remove allow_reuse flag from status field validator

* refactor: simplify field validators by removing pre and always flags

* refactor: update user model imports and enhance account class with abstract method

* refactor: update model_config to use dictionary format in Admin and SubscriptionUserResponse classes

* fix typo in UserDataResetByNext

* change pre=True to mode="before"

* refactor: update validation methods and model configuration in User and Proxy classes

* change pre=False with mode="after"

* Migrated to Pydantic V2

* fix: custom subscriptions not workong

* some small changes

* add missing properties to example schema

* replace from_orm with model_validate

---------

Co-authored-by: MahdiButcher <madibutchercoding@gmail.com>
Co-authored-by: Mahdi Butcher <MahdiButcherCoding@gmail.com>
2024-12-09 21:56:24 +03:30

133 lines
3.9 KiB
Python

from typing import Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext
from pydantic import ConfigDict, field_validator, BaseModel
from app.db import Session, crud, get_db
from app.utils.jwt import get_admin_payload
from config import SUDOERS
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/admin/token") # Admin view url
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
class Admin(BaseModel):
username: str
is_sudo: bool
telegram_id: Optional[int] = None
discord_webhook: Optional[str] = None
users_usage: Optional[int] = None
model_config = ConfigDict(from_attributes=True)
@classmethod
def get_admin(cls, token: str, db: Session):
payload = get_admin_payload(token)
if not payload:
return
if payload['username'] in SUDOERS and payload['is_sudo'] is True:
return cls(username=payload['username'], is_sudo=True)
dbadmin = crud.get_admin(db, payload['username'])
if not dbadmin:
return
if dbadmin.password_reset_at:
if not payload.get("created_at"):
return
if dbadmin.password_reset_at > payload.get("created_at"):
return
return cls.model_validate(dbadmin)
@classmethod
def get_current(cls,
db: Session = Depends(get_db),
token: str = Depends(oauth2_scheme)):
admin = cls.get_admin(token, db)
if not admin:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return admin
@classmethod
def check_sudo_admin(cls,
db: Session = Depends(get_db),
token: str = Depends(oauth2_scheme)):
admin = cls.get_admin(token, db)
if not admin:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
if not admin.is_sudo:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You're not allowed"
)
return admin
class AdminCreate(Admin):
password: str
telegram_id: Optional[int] = None
discord_webhook: Optional[str] = None
@property
def hashed_password(self):
return pwd_context.hash(self.password)
@field_validator("discord_webhook")
@classmethod
def validate_discord_webhook(cls, value):
if value and not value.startswith("https://discord.com"):
raise ValueError("Discord webhook must start with 'https://discord.com'")
return value
class AdminModify(BaseModel):
password: Optional[str] = None
is_sudo: bool
telegram_id: Optional[int] = None
discord_webhook: Optional[str] = None
@property
def hashed_password(self):
if self.password:
return pwd_context.hash(self.password)
@field_validator("discord_webhook")
@classmethod
def validate_discord_webhook(cls, value):
if value and not value.startswith("https://discord.com"):
raise ValueError("Discord webhook must start with 'https://discord.com'")
return value
class AdminPartialModify(AdminModify):
__annotations__ = {k: Optional[v] for k, v in AdminModify.__annotations__.items()}
class AdminInDB(Admin):
username: str
hashed_password: str
def verify_password(self, plain_password):
return pwd_context.verify(plain_password, self.hashed_password)
class AdminValidationResult(BaseModel):
username: str
is_sudo: bool