diff --git a/app/marzneshin.py b/app/marzneshin.py
@@ -1,7 +1,5 @@
import asyncio
-import atexit
import logging
-import os
from datetime import datetime as dt
from datetime import timedelta as td
@@ -11,6 +9,7 @@ from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse
+from fastapi_pagination import add_pagination
from starlette.staticfiles import StaticFiles
from uvicorn import Config, Server
@@ -19,7 +18,6 @@ from app.templates import render_template
from config import (DEBUG, DOCS, HOME_PAGE_TEMPLATE, UVICORN_HOST,
UVICORN_PORT, UVICORN_SSL_CERTFILE, UVICORN_SSL_KEYFILE,
UVICORN_UDS)
-
from . import __version__, telegram
from .routes import api_router
from .tasks import (delete_expired_reminders, nodes_startup,
@@ -38,6 +36,7 @@ app = FastAPI(
)
app.include_router(api_router)
+add_pagination(app)
@app.get("/", response_class=HTMLResponse)
diff --git a/app/models/node.py b/app/models/node.py
@@ -8,7 +8,6 @@ class NodeStatus(str, Enum):
healthy = "healthy"
unhealthy = "unhealthy"
disabled = "disabled"
- none = "none"
class NodeConnectionBackend(str, Enum):
diff --git a/app/models/user.py b/app/models/user.py
@@ -196,11 +196,6 @@ class UserResponse(User):
salt = secrets.token_hex(8)
url_prefix = (XRAY_SUBSCRIPTION_URL_PREFIX).replace('*', salt)
return f"{url_prefix}/sub/{self.username}/{self.key}"
-
-
-class UsersResponse(BaseModel):
- total: int
- users: List[UserResponse]
class UserUsageResponse(BaseModel):
diff --git a/app/routes/admin.py b/app/routes/admin.py
@@ -1,11 +1,14 @@
-from typing import List, Optional, Annotated
+from typing import Optional, Annotated
import sqlalchemy
from fastapi import APIRouter
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
+from fastapi_pagination import Page
+from fastapi_pagination.ext.sqlalchemy import paginate
from app.db import Session, crud
+from app.db.models import Admin as DBAdmin
from app.dependencies import AdminDep, SudoAdminDep, DBDep
from app.models.admin import Admin, AdminCreate, AdminInDB, AdminModify, Token
from app.utils.jwt import create_admin_token
@@ -29,6 +32,34 @@ def authenticate_admin(db: Session, username: str, password: str) -> Optional[Ad
return dbadmin if AdminInDB.model_validate(dbadmin).verify_password(password) else None
+@router.get("", response_model=Page[Admin])
+def get_admins(db: DBDep,
+ admin: SudoAdminDep,
+ username: str = None):
+ query = db.query(DBAdmin)
+ if username:
+ query = query.filter(DBAdmin.username.ilike(f'%{username}%'))
+ return paginate(db, query)
+
+
+@router.post("", response_model=Admin)
+def create_admin(new_admin: AdminCreate,
+ db: DBDep,
+ admin: SudoAdminDep):
+ try:
+ dbadmin = crud.create_admin(db, new_admin)
+ except sqlalchemy.exc.IntegrityError:
+ db.rollback()
+ raise HTTPException(status_code=409, detail="Admin already exists")
+
+ return dbadmin
+
+
+@router.get("/current", response_model=Admin)
+def get_current_admin(admin: AdminDep):
+ return admin
+
+
@router.post("/token", response_model=Token)
def admin_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: DBDep):
@@ -45,19 +76,6 @@ def admin_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
)
-@router.post("", response_model=Admin)
-def create_admin(new_admin: AdminCreate,
- db: DBDep,
- admin: SudoAdminDep):
- try:
- dbadmin = crud.create_admin(db, new_admin)
- except sqlalchemy.exc.IntegrityError:
- db.rollback()
- raise HTTPException(status_code=409, detail="Admin already exists")
-
- return dbadmin
-
-
@router.put("/{username}", response_model=Admin)
def modify_admin(username: str,
modified_admin: AdminModify,
@@ -101,17 +119,3 @@ def remove_admin(username: str,
crud.remove_admin(db, dbadmin)
return {}
-
-
-@router.get("/current", response_model=Admin)
-def get_current_admin(admin: AdminDep):
- return admin
-
-
-@router.get("", response_model=List[Admin])
-def get_admins(db: DBDep,
- admin: SudoAdminDep,
- offset: int = None,
- limit: int = None,
- username: str = None):
- return crud.get_admins(db, offset, limit, username)
diff --git a/app/routes/inbounds.py b/app/routes/inbounds.py
@@ -1,27 +1,31 @@
-from fastapi import APIRouter, Depends
+from fastapi import APIRouter, Depends, Query
from fastapi import HTTPException
+from fastapi_pagination.ext.sqlalchemy import paginate
+from fastapi_pagination.links import Page
from app.db import crud
+from app.db.models import InboundHost as DBInboundHost, Inbound as DBInbound
from app.dependencies import DBDep, sudo_admin
from app.models.proxy import Inbound, InboundHost, InboundHostResponse
-from app.db.models import InboundHost as DBInboundHost
router = APIRouter(prefix="/inbounds", dependencies=[Depends(sudo_admin)], tags=['Inbounds'])
-@router.get("", response_model=list[Inbound])
-def get_inbounds(db: DBDep):
+@router.get("", response_model=Page[Inbound])
+def get_inbounds(db: DBDep, tag: str = Query(None)):
"""
Get all inbounds
"""
- inbounds = crud.get_all_inbounds(db)
+ query = db.query(DBInbound)
+ if tag:
+ query = query.filter(DBInbound.tag.ilike(f"%{tag}%"))
- return inbounds
+ return paginate(db, query)
-@router.get('/hosts', response_model=list[InboundHost])
+@router.get('/hosts', response_model=Page[InboundHost])
def get_hosts(db: DBDep):
- return db.query(DBInboundHost).all()
+ return paginate(db.query(DBInboundHost))
@router.put("/hosts/{id}", response_model=InboundHostResponse)
@@ -63,19 +67,16 @@ def get_inbound(id: int, db: DBDep):
return inbound
-@router.get("/{id}/hosts", response_model=list[InboundHostResponse])
+@router.get("/{id}/hosts", response_model=Page[InboundHostResponse])
def get_inbound_hosts(id: int | None, db: DBDep):
"""
Get hosts of a specific inbound
"""
- if not id:
- print()
- return
inbound = crud.get_inbound(db, id)
if not inbound:
raise HTTPException(status_code=404, detail="Inbound not found")
- return inbound.hosts
+ return paginate(db, db.query(DBInboundHost).filter(DBInboundHost.inbound_id == id))
@router.post("/{id}/hosts", response_model=InboundHostResponse)
diff --git a/app/routes/node.py b/app/routes/node.py
@@ -1,19 +1,18 @@
import json
import logging
-from datetime import datetime
-from typing import List, Annotated
+from typing import Annotated
import sqlalchemy
-from fastapi import APIRouter, Depends, Body
+from fastapi import APIRouter, Body, Query
from fastapi import HTTPException, WebSocket
-from starlette.requests import Request
-from starlette.responses import PlainTextResponse
+from fastapi_pagination.ext.sqlalchemy import paginate
+from fastapi_pagination.links import Page
from app import marznode
from app.db import crud, get_tls_certificate
-from app.dependencies import DBDep, SudoAdminDep, sudo_admin, EndDateDep, StartDateDep, get_admin
-from app.marznode import MarzNodeGRPCIO
-from app.models.admin import Admin
+from app.db.models import Node
+from app.dependencies import DBDep, SudoAdminDep, EndDateDep, StartDateDep, get_admin
+
from app.models.node import (NodeCreate, NodeModify, NodeResponse,
NodeSettings, NodeStatus, NodesUsageResponse)
@@ -21,6 +20,39 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/nodes", tags=['Node'])
+@router.get("", response_model=Page[NodeResponse])
+def get_nodes(db: DBDep,
+ admin: SudoAdminDep,
+ status: list[NodeStatus] = Query(None),
+ name: str = Query(None)):
+ query = db.query(Node)
+
+ if name:
+ query = query.filter(Node.name.ilike(f"%{name}%"))
+
+ if status:
+ query = query.filter(Node.status.in_(status))
+
+ return paginate(db, query)
+
+
+@router.post("", response_model=NodeResponse)
+async def add_node(new_node: NodeCreate,
+ db: DBDep,
+ admin: SudoAdminDep):
+ try:
+ db_node = crud.create_node(db, new_node)
+ except sqlalchemy.exc.IntegrityError:
+ db.rollback()
+ raise HTTPException(status_code=409, detail=f"Node \"{new_node.name}\" already exists")
+ certificate = get_tls_certificate(db)
+
+ await marznode.operations.add_node(db_node, certificate)
+
+ logger.info("New node `%s` added", db_node.name)
+ return db_node
+
+
@router.get("/settings", response_model=NodeSettings)
def get_node_settings(db: DBDep,
admin: SudoAdminDep):
@@ -44,23 +76,6 @@ def get_usage(db: DBDep,
return {"usages": usages}
-@router.post("", response_model=NodeResponse)
-async def add_node(new_node: NodeCreate,
- db: DBDep,
- admin: SudoAdminDep):
- try:
- db_node = crud.create_node(db, new_node)
- except sqlalchemy.exc.IntegrityError:
- db.rollback()
- raise HTTPException(status_code=409, detail=f"Node \"{new_node.name}\" already exists")
- certificate = get_tls_certificate(db)
-
- await marznode.operations.add_node(db_node, certificate)
-
- logger.info("New node `%s` added", db_node.name)
- return db_node
-
-
@router.get("/{node_id}", response_model=NodeResponse)
def get_node(node_id: int,
db: DBDep,
@@ -98,12 +113,6 @@ async def node_logs(node_id: int,
await websocket.send_text(line)
-@router.get("", response_model=List[NodeResponse])
-def get_nodes(db: DBDep,
- admin: SudoAdminDep):
- return crud.get_nodes(db)
-
-
@router.put("/{node_id}", response_model=NodeResponse)
async def modify_node(node_id: int,
modified_node: NodeModify,
@@ -124,6 +133,21 @@ async def modify_node(node_id: int,
return db_node
+@router.delete("/{node_id}")
+async def remove_node(node_id: int,
+ db: DBDep,
+ admin: SudoAdminDep):
+ db_node = crud.get_node_by_id(db, node_id)
+ if not db_node:
+ raise HTTPException(status_code=404, detail="Node not found")
+
+ crud.remove_node(db, db_node)
+ await marznode.operations.remove_node(db_node.id)
+
+ logger.info(f"Node `%s` deleted", db_node.name)
+ return {}
+
+
@router.post("/{node_id}/resync")
async def reconnect_node(node_id: int,
db: DBDep,
@@ -154,18 +178,3 @@ async def alter_node_xray_config(node_id: int,
xray_config = json.dumps(body)
await node.restart_xray(xray_config)
return {}
-
-
-@router.delete("/{node_id}")
-async def remove_node(node_id: int,
- db: DBDep,
- admin: SudoAdminDep):
- db_node = crud.get_node_by_id(db, node_id)
- if not db_node:
- raise HTTPException(status_code=404, detail="Node not found")
-
- crud.remove_node(db, db_node)
- await marznode.operations.remove_node(db_node.id)
-
- logger.info(f"Node `%s` deleted", db_node.name)
- return {}
diff --git a/app/routes/service.py b/app/routes/service.py
@@ -1,11 +1,13 @@
-from typing import List
-
import sqlalchemy
-from fastapi import APIRouter, Depends
+from fastapi import APIRouter, Depends, Query
from fastapi import HTTPException
+from fastapi_pagination.customization import UseParamsFields, CustomizedPage
+from fastapi_pagination.ext.sqlalchemy import paginate
+from fastapi_pagination.links import Page
from app import marznode
from app.db import crud
+from app.db.models import Service
from app.dependencies import DBDep, sudo_admin
from app.models.service import (ServiceCreate, ServiceModify,
ServiceResponse)
@@ -13,6 +15,14 @@ from app.models.service import (ServiceCreate, ServiceModify,
router = APIRouter(prefix="/services", dependencies=[Depends(sudo_admin)], tags=['Service'])
+@router.get("", response_model=Page[ServiceResponse])
+def get_services(db: DBDep, name: str = Query(None)):
+ query = db.query(Service)
+ if name:
+ query = query.filter(Service.name.ilike(f"%{name}%"))
+ return paginate(query)
+
+
@router.post("", response_model=ServiceResponse)
def add_service(new_service: ServiceCreate,
db: DBDep):
@@ -77,10 +87,3 @@ def remove_service(id: int,
crud.remove_service(db, dbservice)
return dict()
-
-
-@router.get("", response_model=List[ServiceResponse])
-def get_services(db: DBDep,
- offset: int = None,
- limit: int = None):
- return crud.get_services(db) # , offset, limit)
diff --git a/app/routes/system.py b/app/routes/system.py
@@ -1,13 +1,11 @@
-from typing import Dict, List, Union
+from typing import Union
from fastapi import APIRouter
from app import __version__
-# from .. import __version__
from app.db import crud
-from app.dependencies import DBDep, AdminDep, SudoAdminDep
+from app.dependencies import DBDep, AdminDep
from app.models.admin import Admin
-from app.models.proxy import InboundHost, Inbound
from app.models.system import SystemStats
from app.models.user import UserStatus
from app.utils.system import memory_usage, cpu_usage, realtime_bandwidth
@@ -39,31 +37,3 @@ def get_system_stats(db: DBDep, admin: AdminDep):
incoming_bandwidth_speed=realtime_bandwidth_stats.incoming_bytes,
outgoing_bandwidth_speed=realtime_bandwidth_stats.outgoing_bytes,
)
-
-
-@router.get("/hosts", response_model=Dict[int, List[InboundHost]])
-def get_hosts(db: DBDep, admin: SudoAdminDep):
- hosts = crud.get_all_hosts(db)
-
- return hosts
-
-
-@router.put('/api/hosts', response_model=Dict[str, List[InboundHost]])
-def modify_hosts(modified_hosts: Dict[str, List[InboundHost]],
- db: DBDep,
- admin: SudoAdminDep):
- # validate
- # for inbound_id, hosts in modified_hosts.items():
- # if not xray.config.inbounds_by_id.get(inbound_id):
- # raise HTTPException(status_code=400, detail=f"Inbound {inbound_id} doesn't exist")
-
- for inbound_id, hosts in modified_hosts.items():
- crud.update_hosts(db, inbound_id, hosts)
-
- # xray.hosts.update()
-
- hosts = {}
- # for inbound_tag in xray.config.inbounds_by_tag:
- # hosts[inbound_tag] = crud.get_hosts(db, inbound_tag)
-
- return hosts
diff --git a/app/routes/user.py b/app/routes/user.py
@@ -1,23 +1,70 @@
import asyncio
import logging
from datetime import datetime, timezone
-from typing import List
+from enum import Enum
import sqlalchemy
from fastapi import APIRouter
from fastapi import HTTPException, Query
+from fastapi_pagination.ext.sqlalchemy import paginate
+from fastapi_pagination.links import Page
from app import marznode
-from app.db import crud
+from app.db import crud, User
from app.dependencies import DBDep, AdminDep, SudoAdminDep, UserDep, StartDateDep, EndDateDep
from app.models.user import (UserCreate, UserModify, UserResponse,
- UsersResponse, UserStatus, UserUsagesResponse)
+ UserStatus, UserUsagesResponse)
from app.utils import report
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/users", tags=['User'])
+class UsersSortingOptions(str, Enum):
+ username = "username"
+ used_traffic = "used_traffic"
+ data_limit = "data_limit"
+ expire = "expire"
+ created_at = "created_at"
+
+
+@router.get("", response_model=Page[UserResponse])
+def get_users(db: DBDep,
+ admin: AdminDep,
+ username: list[str] = Query(None),
+ status: list[UserStatus] = Query(None),
+ order_by: UsersSortingOptions = Query(None),
+ descending: bool = Query(False)):
+ """
+ Filters users based on the options
+ """
+ dbadmin = crud.get_admin(db, admin.username)
+
+ query = db.query(User)
+
+ admin = dbadmin if not admin.is_sudo else None
+
+ if username:
+ if len(username) > 1:
+ query = query.filter(User.username.in_(username))
+ else:
+ query = query.filter(User.username.ilike(f"%{username[0]}%"))
+
+ if status:
+ query = query.filter(User.status.in_(status))
+
+ if admin:
+ query = query.filter(User.admin == admin)
+
+ if order_by:
+ order_column = getattr(User, order_by)
+ if descending:
+ order_column = order_column.desc()
+ query = query.order_by(order_column)
+
+ return paginate(db, query)
+
+
@router.post("", response_model=UserResponse)
async def add_user(new_user: UserCreate,
db: DBDep,
@@ -51,6 +98,55 @@ async def add_user(new_user: UserCreate,
return user
+@router.post("/reset")
+async def reset_users_data_usage(db: DBDep,
+ admin: SudoAdminDep):
+ """
+ Reset all users data usage,
+ You will need to restart for this to take effect for now
+ """
+ dbadmin = crud.get_admin(db, admin.username)
+ crud.reset_all_users_data_usage(db=db, admin=dbadmin)
+ return {}
+
+
+@router.delete("/expired")
+def delete_expired(passed_time: int,
+ db: DBDep,
+ admin: AdminDep):
+ """
+ Delete expired users
+ - **passed_time** must be a timestamp
+ - This function will delete all expired users that meet the specified number of days passed and can't be undone.
+ """
+
+ dbadmin = crud.get_admin(db, admin.username)
+
+ db_users = crud.get_users(db=db,
+ status=[UserStatus.expired, UserStatus.limited],
+ admin=dbadmin if not admin.is_sudo else None)
+
+ current_time = int(datetime.now(timezone.utc).timestamp())
+ expiration_threshold = current_time - passed_time
+ expired_users = [
+ user for user in db_users if user.expire is not None and user.expire <= expiration_threshold]
+ if not expired_users:
+ raise HTTPException(status_code=404, detail="No expired user found.")
+
+ for db_user in expired_users:
+ crud.remove_user(db, db_user)
+
+ asyncio.create_task(
+ report.user_deleted(
+ username=db_user.username,
+ by=admin
+ ))
+
+ logger.info("User `%s` removed", db_user.username)
+
+ return {}
+
+
@router.get("/{username}", response_model=UserResponse)
def get_user(db_user: UserDep):
"""
@@ -174,53 +270,6 @@ async def revoke_user_subscription(db_user: UserDep,
return user
-@router.get("", response_model=UsersResponse)
-def get_users(db: DBDep,
- admin: AdminDep,
- offset: int = None,
- limit: int = None,
- username: List[str] = Query(None),
- status: UserStatus = None,
- sort: str = None):
- """
- Get all users
- """
- dbadmin = crud.get_admin(db, admin.username)
-
- if sort is not None:
- opts = sort.strip(',').split(',')
- sort = []
- for opt in opts:
- try:
- sort.append(crud.UsersSortingOptions[opt])
- except KeyError:
- raise HTTPException(status_code=400,
- detail=f'"{opt}" is not a valid sort option')
-
- users, count = crud.get_users(db=db,
- offset=offset,
- limit=limit,
- usernames=username,
- status=status,
- sort=sort,
- admin=dbadmin if not admin.is_sudo else None,
- return_with_count=True)
-
- return {"users": users, "total": count}
-
-
-@router.post("/reset")
-async def reset_users_data_usage(db: DBDep,
- admin: SudoAdminDep):
- """
- Reset all users data usage,
- You will need to restart for this to take effect for now
- """
- dbadmin = crud.get_admin(db, admin.username)
- crud.reset_all_users_data_usage(db=db, admin=dbadmin)
- return {}
-
-
@router.get("/{username}/usage", response_model=UserUsagesResponse)
def get_user_usage(db: DBDep,
db_user: UserDep,
@@ -253,40 +302,3 @@ def set_owner(username: str,
logger.info("`%s`'s owner successfully set to `%s`", user.username, admin.username)
return user
-
-
-@router.delete("/expired")
-def delete_expired(passed_time: int,
- db: DBDep,
- admin: AdminDep):
- """
- Delete expired users
- - **passed_time** must be a timestamp
- - This function will delete all expired users that meet the specified number of days passed and can't be undone.
- """
-
- dbadmin = crud.get_admin(db, admin.username)
-
- db_users = crud.get_users(db=db,
- status=[UserStatus.expired, UserStatus.limited],
- admin=dbadmin if not admin.is_sudo else None)
-
- current_time = int(datetime.now(timezone.utc).timestamp())
- expiration_threshold = current_time - passed_time
- expired_users = [
- user for user in db_users if user.expire is not None and user.expire <= expiration_threshold]
- if not expired_users:
- raise HTTPException(status_code=404, detail="No expired user found.")
-
- for db_user in expired_users:
- crud.remove_user(db, db_user)
-
- asyncio.get_running_loop().create_task(
- report.user_deleted(
- username=db_user.username,
- by=admin
- ))
-
- logger.info("User `%s` removed", db_user.username)
-
- return {}