diff --git a/app/db/crud.py b/app/db/crud.py index aa912bc..a406a62 100644 --- a/app/db/crud.py +++ b/app/db/crud.py @@ -1,4 +1,5 @@ import json +import secrets from datetime import datetime from enum import Enum from typing import Dict, List, Optional, Tuple, Union @@ -216,15 +217,9 @@ def get_users(db: Session, def get_user_usages(db: Session, dbuser: User, start: datetime, end: datetime, ) -> List[UserUsageResponse]: - usages = {} + usages = dict() - usages[0] = UserUsageResponse( # Main Core - node_id=None, - node_name="Master", - used_traffic=0 - ) for node in db.query(Node).all(): - usages[node.id] = UserUsageResponse( node_id=node.id, node_name=node.name, @@ -362,14 +357,7 @@ def reset_user_data_usage(db: Session, dbuser: User): def revoke_user_sub(db: Session, dbuser: User): - dbuser.sub_revoked_at = datetime.utcnow() - - user = UserResponse.model_validate(dbuser) - for proxy_type, settings in user.proxies.copy().items(): - settings.revoke() - user.proxies[proxy_type] = settings - dbuser = update_user(db, dbuser, user) - + dbuser.key = secrets.token_hex(16) db.commit() db.refresh(dbuser) return dbuser diff --git a/app/db/migrations/versions/20faa9f18c0a_init.py b/app/db/migrations/versions/20faa9f18c0a_init.py index 0e2a38c..adefe4b 100644 --- a/app/db/migrations/versions/20faa9f18c0a_init.py +++ b/app/db/migrations/versions/20faa9f18c0a_init.py @@ -115,7 +115,6 @@ def upgrade() -> None: sa.Column('settings', sa.String(), nullable=True), sa.Column('expire', sa.Integer(), nullable=True), sa.Column('admin_id', sa.Integer(), nullable=True), - sa.Column('sub_revoked_at', sa.DateTime(), nullable=True), sa.Column('sub_updated_at', sa.DateTime(), nullable=True), sa.Column('sub_last_user_agent', sa.String(length=512), nullable=True), sa.Column('created_at', sa.DateTime(), nullable=True), diff --git a/app/db/models.py b/app/db/models.py index 02fcd1e..f2a9eea 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -83,7 +83,6 @@ class User(Base): expire = Column(Integer, nullable=True) admin_id = Column(Integer, ForeignKey("admins.id")) admin = relationship("Admin", back_populates="users") - sub_revoked_at = Column(DateTime, nullable=True, default=None) sub_updated_at = Column(DateTime, nullable=True, default=None) sub_last_user_agent = Column(String(512), nullable=True, default=None) created_at = Column(DateTime, default=datetime.utcnow) diff --git a/app/marznode/operations.py b/app/marznode/operations.py index f229cb8..a09e144 100644 --- a/app/marznode/operations.py +++ b/app/marznode/operations.py @@ -22,7 +22,7 @@ async def remove_user(user: "DBUser"): for inb in user.inbounds: nodes_set.add(inb.node_id) - print(nodes_set) + for n in nodes_set: await marznode.nodes[n].remove_user(user) diff --git a/app/models/user.py b/app/models/user.py index fb67664..76ea2df 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -7,7 +7,6 @@ from typing import List, Optional, Union from pydantic import field_validator, ConfigDict, BaseModel, Field, validator, computed_field, ValidationInfo from app.models.proxy import InboundBase -from app.utils.jwt import create_subscription_token from config import XRAY_SUBSCRIPTION_PATH, XRAY_SUBSCRIPTION_URL_PREFIX USERNAME_REGEXP = re.compile(r"^(?=\w{3,32}\b)[a-zA-Z0-9-_@.]+(?:_[a-zA-Z0-9-_@.]+)*$") @@ -203,8 +202,7 @@ class UserResponse(User): def subscription_url(self) -> str: salt = secrets.token_hex(8) url_prefix = (XRAY_SUBSCRIPTION_URL_PREFIX).replace('*', salt) - token = create_subscription_token(self.username) - return f"{url_prefix}/{XRAY_SUBSCRIPTION_PATH}/{token}" + return f"{url_prefix}/{XRAY_SUBSCRIPTION_PATH}/{self.username}/{self.key}" class UsersResponse(BaseModel): diff --git a/app/utils/jwt.py b/app/utils/jwt.py index 0edc273..aca1ad2 100644 --- a/app/utils/jwt.py +++ b/app/utils/jwt.py @@ -38,20 +38,3 @@ def get_admin_payload(token: str) -> Union[dict, None]: return {"username": username, "is_sudo": access == "sudo", "created_at": created_at} except JWTError: return - - -def create_subscription_token(username: str) -> str: - data = {"sub": username, "access": "subscription", "iat": datetime.utcnow()+timedelta(seconds=1)} - encoded_jwt = jwt.encode(data, get_secret_key(), algorithm="HS256") - return encoded_jwt - - -def get_subscription_payload(token: str) -> Union[dict, None]: - try: - payload = jwt.decode(token, get_secret_key(), algorithms=["HS256"]) - if payload.get("access") != "subscription": - return - - return {"username": payload['sub'], "created_at": datetime.utcfromtimestamp(payload['iat'])} - except JWTError: - return diff --git a/app/utils/notification.py b/app/utils/notification.py index 327e2ea..da20e6b 100644 --- a/app/utils/notification.py +++ b/app/utils/notification.py @@ -1,7 +1,6 @@ from collections import deque from datetime import datetime as dt from enum import Enum -from typing import Type from pydantic import BaseModel diff --git a/app/views/subscription.py b/app/views/subscription.py index ccb416e..6b1d5b4 100644 --- a/app/views/subscription.py +++ b/app/views/subscription.py @@ -8,7 +8,6 @@ from app import app from app.db import Session, crud, get_db from app.models.user import UserResponse from app.templates import render_template -from app.utils.jwt import get_subscription_payload from app.utils.share import encode_title, generate_subscription from config import ( SUB_PROFILE_TITLE, @@ -19,9 +18,10 @@ from config import ( ) -@app.get("/%s/{token}/" % XRAY_SUBSCRIPTION_PATH, tags=['Subscription']) -@app.get("/%s/{token}" % XRAY_SUBSCRIPTION_PATH, include_in_schema=False) -def user_subscription(token: str, +@app.get("/%s/{username}/{key}/" % XRAY_SUBSCRIPTION_PATH, tags=['Subscription']) +@app.get("/%s/{username}/{key}" % XRAY_SUBSCRIPTION_PATH, include_in_schema=False) +def user_subscription(username: str, + key: str, request: Request, db: Session = Depends(get_db), user_agent: str = Header(default="")): @@ -37,17 +37,14 @@ def user_subscription(token: str, "total": user.data_limit, "expire": user.expire, } + try: + int(key, 16) + except ValueError: + return Response(status_code=404) - sub = get_subscription_payload(token) - if not sub: - return Response(status_code=204) - - dbuser = crud.get_user(db, sub['username']) - if not dbuser or dbuser.created_at > sub['created_at']: - return Response(status_code=204) - - if dbuser.sub_revoked_at and dbuser.sub_revoked_at > sub['created_at']: - return Response(status_code=204) + dbuser = crud.get_user(db, username) + if not dbuser or dbuser.key != key: + return Response(status_code=404) user: UserResponse = UserResponse.model_validate(dbuser) @@ -95,39 +92,37 @@ def user_subscription(token: str, return Response(content=conf, media_type="text/plain", headers=response_headers) -@app.get("/%s/{token}/info" % XRAY_SUBSCRIPTION_PATH, tags=['Subscription'], response_model=UserResponse) -def user_subscription_info(token: str, +@app.get("/%s/{username}/{key}/info" % XRAY_SUBSCRIPTION_PATH, tags=['Subscription'], response_model=UserResponse) +def user_subscription_info(username: str, + key: str, db: Session = Depends(get_db)): - sub = get_subscription_payload(token) - if not sub: - return Response(status_code=404) - - dbuser = crud.get_user(db, sub['username']) - if not dbuser or dbuser.created_at > sub['created_at']: + try: + int(key, 16) + except ValueError: return Response(status_code=404) - elif dbuser.sub_revoked_at and dbuser.sub_revoked_at > sub['created_at']: + dbuser = crud.get_user(db, username) + if not dbuser or key != dbuser.key: return Response(status_code=404) return dbuser -@app.get("/%s/{token}/usage" % XRAY_SUBSCRIPTION_PATH, tags=['Subscription']) -def user_get_usage(token: str, +@app.get("/%s/{username}/{key}/usage" % XRAY_SUBSCRIPTION_PATH, tags=['Subscription']) +def user_get_usage(username: str, + key: str, start: str = None, end: str = None, db: Session = Depends(get_db)): - sub = get_subscription_payload(token) - if not sub: - return Response(status_code=204) - - dbuser = crud.get_user(db, sub['username']) - if not dbuser or dbuser.created_at > sub['created_at']: - return Response(status_code=204) + try: + int(key, 16) + except ValueError: + return Response(status_code=404) - if dbuser.sub_revoked_at and dbuser.sub_revoked_at > sub['created_at']: - return Response(status_code=204) + dbuser = crud.get_user(db, username) + if not dbuser or key != dbuser.key: + return Response(status_code=404) if start is None: start_date = datetime.fromtimestamp(datetime.utcnow().timestamp() - 30 * 24 * 3600) @@ -144,12 +139,13 @@ def user_get_usage(token: str, return {"usages": usages, "username": dbuser.username} -@app.get("/%s/{token}/{client_type}" % XRAY_SUBSCRIPTION_PATH, tags=['Subscription']) +@app.get("/%s/{username}/{key}/{client_type}" % XRAY_SUBSCRIPTION_PATH, tags=['Subscription']) def user_subscription_with_client_type( - token: str, - request: Request, - client_type: str = Path(..., regex="sing-box|clash-meta|clash|outline|v2ray"), - db: Session = Depends(get_db), + username: str, + key: str, + request: Request, + client_type: str = Path(..., regex="sing-box|clash-meta|clash|outline|v2ray"), + db: Session = Depends(get_db), ): """ Subscription link, v2ray, clash, sing-box, outline and clash-meta supported @@ -163,16 +159,14 @@ def user_subscription_with_client_type( "expire": user.expire, } - sub = get_subscription_payload(token) - if not sub: - return Response(status_code=204) - - dbuser = crud.get_user(db, sub['username']) - if not dbuser or dbuser.created_at > sub['created_at']: - return Response(status_code=204) + try: + int(key, 16) + except ValueError: + return Response(status_code=404) - if dbuser.sub_revoked_at and dbuser.sub_revoked_at > sub['created_at']: - return Response(status_code=204) + dbuser = crud.get_user(db, username) + if not dbuser or key != dbuser.key: + return Response(status_code=404) user: UserResponse = UserResponse.model_validate(dbuser) diff --git a/app/views/user.py b/app/views/user.py index 993ec89..17a6e44 100644 --- a/app/views/user.py +++ b/app/views/user.py @@ -179,19 +179,21 @@ async def revoke_user_subscription(username: str, """ Revoke users subscription (Subscription link and proxies) """ - if not (admin.is_sudo or (dbuser.admin and dbuser.admin.username == admin.username)): + db_user = crud.get_user(db, username) + + if not (admin.is_sudo or (db_user.admin and db_user.admin.username == admin.username)): raise HTTPException(status_code=403, detail="You're not allowed") - dbuser = crud.get_user(db, username) - if not dbuser: + if not db_user: raise HTTPException(status_code=404, detail="User not found") - dbuser = crud.revoke_user_sub(db=db, dbuser=dbuser) + db_user = crud.revoke_user_sub(db=db, dbuser=db_user) - if dbuser.status in [UserStatus.active, UserStatus.on_hold]: - asyncio.get_running_loop().create_task(xray.operations.update_user(dbuser=dbuser)) - user = UserResponse.model_validate(dbuser) - asyncio.get_running_loop().create_task( + if db_user.status in [UserStatus.active, UserStatus.on_hold]: + await marznode.operations.remove_user(db_user) + await marznode.operations.add_user(db_user) + user = UserResponse.model_validate(db_user) + asyncio.create_task( report.user_subscription_revoked( user=user, by=admin