mirror of
https://github.com/Gozargah/Marzban.git
synced 2026-05-17 00:25:53 +03:00
* 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>
133 lines
3.9 KiB
Python
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
|